@ty_krystal/sei-ai 0.1.8 → 0.1.12

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 (57) hide show
  1. package/README.md +1 -21
  2. package/dist/commands/api-docs.d.ts +1 -1
  3. package/dist/commands/api-docs.js +1 -1
  4. package/dist/commands/call.d.ts +1 -1
  5. package/dist/commands/call.js +1 -1
  6. package/dist/commands/dict/add-category.d.ts +1 -1
  7. package/dist/commands/dict/add-category.js +1 -1
  8. package/dist/commands/dict/add-item.d.ts +1 -1
  9. package/dist/commands/dict/add-item.js +1 -1
  10. package/dist/commands/dict/list.d.ts +1 -1
  11. package/dist/commands/dict/list.js +1 -1
  12. package/dist/commands/init/base-data.d.ts +1 -1
  13. package/dist/commands/init/base-data.js +1 -1
  14. package/dist/commands/query-sql.d.ts +1 -1
  15. package/dist/commands/query-sql.js +1 -1
  16. package/dist/commands/query.d.ts +1 -1
  17. package/dist/commands/query.js +1 -1
  18. package/dist/commands/relogin.d.ts +1 -1
  19. package/dist/commands/relogin.js +1 -1
  20. package/dist/commands/save.d.ts +1 -1
  21. package/dist/commands/save.js +1 -1
  22. package/dist/commands.d.ts +24 -0
  23. package/dist/commands.js +23 -0
  24. package/dist/core/cli-actions.d.ts +94 -0
  25. package/dist/core/cli-actions.js +155 -0
  26. package/dist/core/cli-helpers.d.ts +39 -0
  27. package/dist/core/cli-helpers.js +246 -0
  28. package/dist/core/command-base/context.d.ts +6 -0
  29. package/dist/core/command-base/context.js +10 -0
  30. package/dist/core/command-base/output.d.ts +2 -0
  31. package/dist/core/command-base/output.js +6 -0
  32. package/dist/core/command-base/payload.d.ts +7 -0
  33. package/dist/core/command-base/payload.js +33 -0
  34. package/dist/core/command-base/sei-command.d.ts +42 -0
  35. package/dist/core/command-base/sei-command.js +88 -0
  36. package/dist/core/config.d.ts +3 -0
  37. package/dist/core/config.js +82 -0
  38. package/dist/core/constants.d.ts +35 -0
  39. package/dist/core/constants.js +48 -0
  40. package/dist/core/env.d.ts +1 -0
  41. package/dist/core/env.js +55 -0
  42. package/dist/core/errors.d.ts +10 -0
  43. package/dist/core/errors.js +55 -0
  44. package/dist/core/index.d.ts +14 -0
  45. package/dist/core/index.js +14 -0
  46. package/dist/core/logger.d.ts +2 -0
  47. package/dist/core/logger.js +71 -0
  48. package/dist/core/openapi.d.ts +2 -0
  49. package/dist/core/openapi.js +261 -0
  50. package/dist/core/sei-client.d.ts +25 -0
  51. package/dist/core/sei-client.js +535 -0
  52. package/dist/core/types.d.ts +139 -0
  53. package/dist/core/types.js +1 -0
  54. package/dist/core/utils.d.ts +12 -0
  55. package/dist/core/utils.js +53 -0
  56. package/oclif.manifest.json +376 -328
  57. package/package.json +14 -8
@@ -0,0 +1,535 @@
1
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { ACCOUNT_CHECK_CODE_PATH, ACCOUNT_LOGIN_DEFAULT_CHECKCODE, ACCOUNT_LOGIN_DEFAULT_OS, ACCOUNT_LOGIN_DEFAULT_PROJECT, ACCOUNT_LOGIN_DEFAULT_TYPE, DDL_EXECUTE_PATH, DEFAULT_AI_KEY, DEFAULT_QUERY_SQL_PAGE, DEFAULT_QUERY_SQL_SIZE, DEFAULT_TIMEOUT_MS, LOGIN_PATH, OPENAPI_DOCS_PATH, QUERY_PATH, QUERY_SQL_PATH, SAVE_PATH, TOKEN_CACHE_DIR_NAME, TOKEN_CACHE_FILE_PREFIX, TOKEN_CACHE_FILE_SUFFIX, TOKEN_CACHE_VERSION, } from './constants.js';
6
+ import { SeiMcpError } from './errors.js';
7
+ import { makeUrl, normalizeBaseUrl, toPositiveInteger, truncateText } from './utils.js';
8
+ export function createSeiClient(config, logger, fetchImpl = fetch) {
9
+ const settings = createSeiAuthSettings(config);
10
+ const auth = createAuthManager(settings, logger, fetchImpl, { useCache: false });
11
+ return {
12
+ query(payload) {
13
+ return requestSeiJson(settings, logger, fetchImpl, auth, 'POST', QUERY_PATH, payload, {
14
+ allowFailure: false,
15
+ });
16
+ },
17
+ save(payload) {
18
+ return requestSeiJson(settings, logger, fetchImpl, auth, 'POST', SAVE_PATH, payload, {
19
+ allowFailure: false,
20
+ });
21
+ },
22
+ querySql(payload) {
23
+ return requestSeiJson(settings, logger, fetchImpl, auth, 'POST', QUERY_SQL_PATH, buildQuerySqlPayload(payload.sql, payload.page, payload.size), {
24
+ allowFailure: false,
25
+ });
26
+ },
27
+ describeTable(tableName) {
28
+ const sql = `SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT ` +
29
+ `FROM information_schema.COLUMNS ` +
30
+ `WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '${escapeSqlLiteral(tableName)}' ` +
31
+ `ORDER BY ORDINAL_POSITION`;
32
+ return requestSeiJson(settings, logger, fetchImpl, auth, 'POST', QUERY_SQL_PATH, buildQuerySqlPayload(sql, 1, 200), {
33
+ allowFailure: false,
34
+ });
35
+ },
36
+ };
37
+ }
38
+ export function createSeiAuthSettings(config, options = {}) {
39
+ return {
40
+ baseUrl: normalizeBaseUrl(options.baseUrl ?? config.baseUrl),
41
+ mode: config.auth.mode,
42
+ token: firstText(options.token, config.auth.token, process.env.SEI_TOKEN),
43
+ aiKey: firstText(options.aiKey, config.auth.aiKey, process.env.SEI_AI_LOGIN_KEY, DEFAULT_AI_KEY),
44
+ role: resolveRole(options.role, config.auth.role),
45
+ account: firstText(options.account, config.auth.account, process.env.SEI_AI_LOGIN_ACCOUNT),
46
+ accountProject: firstText(options.accountProject, process.env.SEI_AI_LOGIN_ACCOUNT_PROJECT, ACCOUNT_LOGIN_DEFAULT_PROJECT),
47
+ accountCheckcode: firstText(options.accountCheckcode, process.env.SEI_AI_LOGIN_ACCOUNT_CHECKCODE, ACCOUNT_LOGIN_DEFAULT_CHECKCODE),
48
+ timeoutMs: normalizeTimeout(options.timeoutMs),
49
+ };
50
+ }
51
+ export function createCliSeiClient(settings, logger, fetchImpl = fetch) {
52
+ const auth = createAuthManager(settings, logger, fetchImpl, { useCache: true });
53
+ return {
54
+ async call(method, path, payload, options = {}) {
55
+ return requestSeiJson(settings, logger, fetchImpl, auth, method, path, payload, options);
56
+ },
57
+ async query(payload, options = {}) {
58
+ return requestSeiJson(settings, logger, fetchImpl, auth, 'POST', QUERY_PATH, payload, options);
59
+ },
60
+ async save(payload, options = {}) {
61
+ return requestSeiJson(settings, logger, fetchImpl, auth, 'POST', SAVE_PATH, payload, options);
62
+ },
63
+ async querySql(sql, page, size, options = {}) {
64
+ return requestSeiJson(settings, logger, fetchImpl, auth, 'POST', QUERY_SQL_PATH, buildQuerySqlPayload(sql, page, size), options);
65
+ },
66
+ async readApiDocs() {
67
+ return requestJsonRaw(settings, logger, fetchImpl, 'GET', OPENAPI_DOCS_PATH, undefined, {
68
+ skipAuth: true,
69
+ });
70
+ },
71
+ async relogin() {
72
+ if (settings.mode === 'token') {
73
+ throw new SeiMcpError('token 模式不支持重新登录,请改用 --token 或 ai-login 模式', 'INVALID_REQUEST');
74
+ }
75
+ const state = await loginThroughSettings(settings, logger, fetchImpl, { useCache: true });
76
+ return state.token;
77
+ },
78
+ async executeDdlStatements(statements, options = {}) {
79
+ const results = [];
80
+ for (const [index, statement] of statements.entries()) {
81
+ const response = await requestSeiJson(settings, logger, fetchImpl, auth, 'POST', DDL_EXECUTE_PATH, { ddl: statement }, options);
82
+ results.push({
83
+ index: index + 1,
84
+ response,
85
+ });
86
+ }
87
+ return results;
88
+ },
89
+ };
90
+ }
91
+ export function buildQuerySqlPayload(sql, page, size) {
92
+ const normalizedSql = stripTrailingQuerySqlLimit(toText(sql));
93
+ if (!normalizedSql) {
94
+ throw new SeiMcpError('SQL 不能为空', 'INVALID_PARAMS');
95
+ }
96
+ return {
97
+ sql: normalizedSql,
98
+ page: toPositiveInteger(page, DEFAULT_QUERY_SQL_PAGE),
99
+ size: toPositiveInteger(size, DEFAULT_QUERY_SQL_SIZE),
100
+ };
101
+ }
102
+ export function stripTrailingQuerySqlLimit(sql) {
103
+ return sql
104
+ .trim()
105
+ .replace(/\s+LIMIT\s+\d+(?:\s*,\s*\d+|\s+OFFSET\s+\d+)?\s*;?\s*$/i, '')
106
+ .replace(/;+\s*$/g, '')
107
+ .trim();
108
+ }
109
+ async function requestSeiJson(settings, logger, fetchImpl, auth, method, path, payload, options = {}) {
110
+ const response = await requestJsonRaw(settings, logger, fetchImpl, method, path, payload, options, auth);
111
+ validateSeiEnvelope(response, path, options.allowFailure === true, options.allowEmpty === true);
112
+ return response;
113
+ }
114
+ async function requestJsonRaw(settings, logger, fetchImpl, method, path, payload, options = {}, auth) {
115
+ const token = options.skipAuth ? null : await resolveRequestToken(settings, auth, options);
116
+ const response = await requestJsonOnce(settings, logger, fetchImpl, method, path, payload, token, options);
117
+ if (!options.skipAuth && auth && isUnauthenticatedResponse(response) && auth.canRefresh()) {
118
+ const refreshedToken = await auth.refreshToken();
119
+ const retried = await requestJsonOnce(settings, logger, fetchImpl, method, path, payload, refreshedToken, options);
120
+ return retried;
121
+ }
122
+ return response;
123
+ }
124
+ async function requestJsonOnce(settings, logger, fetchImpl, method, path, payload, token, options = {}) {
125
+ const controller = new AbortController();
126
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? settings.timeoutMs);
127
+ const headers = {
128
+ Accept: 'application/json, text/plain, */*',
129
+ 'X-Requested-With': 'XMLHttpRequest',
130
+ };
131
+ if (token) {
132
+ headers.token = token;
133
+ headers.Authorization = `Bearer ${token}`;
134
+ }
135
+ if (payload !== undefined && method.toUpperCase() !== 'GET') {
136
+ headers['Content-Type'] = 'application/json';
137
+ }
138
+ if (options.extraHeaders) {
139
+ for (const [key, value] of Object.entries(options.extraHeaders)) {
140
+ headers[key] = value;
141
+ }
142
+ }
143
+ try {
144
+ logger.debug('SEI request', { method: method.toUpperCase(), path, hasToken: Boolean(token) });
145
+ const response = await fetchImpl(makeUrl(settings.baseUrl, path), {
146
+ method: method.toUpperCase(),
147
+ headers,
148
+ body: payload !== undefined && method.toUpperCase() !== 'GET' ? JSON.stringify(payload ?? {}) : undefined,
149
+ signal: controller.signal,
150
+ });
151
+ const text = await response.text();
152
+ if (!response.ok) {
153
+ throw new SeiMcpError(`SEI HTTP ${response.status}`, 'INVALID_REQUEST', {
154
+ status: response.status,
155
+ body: truncateText(text, 2000),
156
+ });
157
+ }
158
+ if (!text.trim()) {
159
+ return null;
160
+ }
161
+ try {
162
+ return JSON.parse(text);
163
+ }
164
+ catch (error) {
165
+ throw new SeiMcpError('SEI 返回不是 JSON', 'INVALID_REQUEST', {
166
+ body: truncateText(text, 2000),
167
+ cause: error instanceof Error ? error.message : String(error),
168
+ });
169
+ }
170
+ }
171
+ catch (error) {
172
+ if (error instanceof SeiMcpError) {
173
+ throw error;
174
+ }
175
+ throw new SeiMcpError('SEI 请求失败', 'INVALID_REQUEST', {
176
+ path,
177
+ cause: error instanceof Error ? error.message : String(error),
178
+ });
179
+ }
180
+ finally {
181
+ clearTimeout(timeout);
182
+ }
183
+ }
184
+ function createAuthManager(settings, logger, fetchImpl, options) {
185
+ let state = null;
186
+ let pending = null;
187
+ return {
188
+ canRefresh() {
189
+ return state?.canRefresh !== false;
190
+ },
191
+ async resolveToken() {
192
+ if (state) {
193
+ return state.token;
194
+ }
195
+ if (!pending) {
196
+ pending = resolveAuthState(settings, logger, fetchImpl, options);
197
+ }
198
+ state = await pending;
199
+ return state.token;
200
+ },
201
+ async refreshToken() {
202
+ if (state?.source === 'explicit' && state.token) {
203
+ return state.token;
204
+ }
205
+ pending = loginThroughSettings(settings, logger, fetchImpl, options);
206
+ state = await pending;
207
+ return state.token;
208
+ },
209
+ };
210
+ }
211
+ async function resolveAuthState(settings, logger, fetchImpl, options) {
212
+ if (settings.token) {
213
+ return {
214
+ token: settings.token,
215
+ source: 'explicit',
216
+ canRefresh: false,
217
+ };
218
+ }
219
+ if (options.useCache) {
220
+ const cachedToken = await loadCachedToken(settings);
221
+ if (cachedToken) {
222
+ return {
223
+ token: cachedToken,
224
+ source: 'cache',
225
+ canRefresh: true,
226
+ };
227
+ }
228
+ }
229
+ return loginThroughSettings(settings, logger, fetchImpl, options);
230
+ }
231
+ async function loginThroughSettings(settings, logger, fetchImpl, options) {
232
+ if (settings.account) {
233
+ const token = await loginAsAccount(settings, logger, fetchImpl);
234
+ if (options.useCache) {
235
+ await saveCachedToken(settings, token, logger);
236
+ }
237
+ return {
238
+ token,
239
+ source: 'account-login',
240
+ canRefresh: true,
241
+ };
242
+ }
243
+ const token = await loginWithAi(settings, logger, fetchImpl, settings.role || null);
244
+ if (options.useCache) {
245
+ await saveCachedToken(settings, token, logger);
246
+ }
247
+ return {
248
+ token,
249
+ source: 'login',
250
+ canRefresh: true,
251
+ };
252
+ }
253
+ async function loginWithAi(settings, logger, fetchImpl, role) {
254
+ if (!settings.aiKey) {
255
+ throw new SeiMcpError('ai-login 模式需要配置 aiKey', 'INVALID_REQUEST');
256
+ }
257
+ const payload = { mcp: settings.aiKey };
258
+ if (role) {
259
+ payload.role = role;
260
+ }
261
+ const response = await requestJsonOnce(settings, logger, fetchImpl, 'POST', LOGIN_PATH, payload, null, {
262
+ timeoutMs: settings.timeoutMs,
263
+ });
264
+ const token = extractToken(response);
265
+ return token;
266
+ }
267
+ async function loginAsAccount(settings, logger, fetchImpl) {
268
+ const adminToken = await loginWithAi(settings, logger, fetchImpl, null);
269
+ const row = await fetchAccountLoginRow(settings, logger, fetchImpl, adminToken, settings.account);
270
+ const passport = toText(getRowField(row, '_UID', 'uid', 'passport')) || settings.account;
271
+ const password = toText(getRowField(row, '_PWD', 'pwd', 'password'));
272
+ if (!password) {
273
+ throw new SeiMcpError(`账号 ${settings.account} 没有可用于登录的 _PWD`, 'INVALID_REQUEST');
274
+ }
275
+ const project = resolveAccountProject(row, settings.accountProject);
276
+ const cookieHeader = await requestCookieHeader(settings, logger, fetchImpl, {
277
+ os: ACCOUNT_LOGIN_DEFAULT_OS,
278
+ project,
279
+ type: ACCOUNT_LOGIN_DEFAULT_TYPE,
280
+ });
281
+ const payload = {
282
+ passport,
283
+ password,
284
+ os: ACCOUNT_LOGIN_DEFAULT_OS,
285
+ project,
286
+ type: ACCOUNT_LOGIN_DEFAULT_TYPE,
287
+ checkcode: settings.accountCheckcode,
288
+ };
289
+ const response = await requestJsonOnce(settings, logger, fetchImpl, 'POST', LOGIN_PATH, payload, null, {
290
+ extraHeaders: cookieHeader ? { Cookie: cookieHeader } : undefined,
291
+ timeoutMs: settings.timeoutMs,
292
+ });
293
+ return extractToken(response);
294
+ }
295
+ async function fetchAccountLoginRow(settings, logger, fetchImpl, adminToken, account) {
296
+ const sql = `SELECT _UID,_PWD,_SYSID,_STATUS FROM _SYS_USER WHERE _UID = '${escapeSqlLiteral(account)}'`;
297
+ const response = await requestJsonOnce(settings, logger, fetchImpl, 'POST', QUERY_SQL_PATH, buildQuerySqlPayload(sql, 1, 1), adminToken, {
298
+ timeoutMs: settings.timeoutMs,
299
+ });
300
+ validateSeiEnvelope(response, `${QUERY_SQL_PATH} 查询账号 ${account}`, false, false);
301
+ const rows = extractResponseRows(response);
302
+ if (rows.length === 0) {
303
+ throw new SeiMcpError(`未找到账号:${account}`, 'INVALID_REQUEST');
304
+ }
305
+ const row = rows[0];
306
+ if (!isObject(row)) {
307
+ throw new SeiMcpError(`账号 ${account} 查询结果不是 JSON 对象`, 'INVALID_REQUEST');
308
+ }
309
+ return row;
310
+ }
311
+ async function requestCookieHeader(settings, logger, fetchImpl, payload) {
312
+ const controller = new AbortController();
313
+ const timeout = setTimeout(() => controller.abort(), settings.timeoutMs);
314
+ try {
315
+ logger.debug('SEI request', { method: 'POST', path: ACCOUNT_CHECK_CODE_PATH, hasToken: false });
316
+ const response = await fetchImpl(makeUrl(settings.baseUrl, ACCOUNT_CHECK_CODE_PATH), {
317
+ method: 'POST',
318
+ headers: {
319
+ Accept: 'application/json, text/plain, */*',
320
+ 'Content-Type': 'application/json',
321
+ 'X-Requested-With': 'XMLHttpRequest',
322
+ },
323
+ body: JSON.stringify(payload),
324
+ signal: controller.signal,
325
+ });
326
+ await response.text();
327
+ if (!response.ok) {
328
+ return null;
329
+ }
330
+ const cookies = response.headers.getSetCookie?.() ?? response.headers.get('set-cookie')?.split(',') ?? [];
331
+ const cookieParts = cookies
332
+ .map((item) => item.split(';', 1)[0]?.trim())
333
+ .filter((item) => Boolean(item));
334
+ return cookieParts.length > 0 ? cookieParts.join('; ') : null;
335
+ }
336
+ finally {
337
+ clearTimeout(timeout);
338
+ }
339
+ }
340
+ async function resolveRequestToken(settings, auth, options) {
341
+ if (typeof options.token === 'string') {
342
+ return options.token;
343
+ }
344
+ if (!auth) {
345
+ return settings.token || null;
346
+ }
347
+ return auth.resolveToken();
348
+ }
349
+ function validateSeiEnvelope(response, context, allowFailure, allowEmpty) {
350
+ if (response === null && allowEmpty) {
351
+ return;
352
+ }
353
+ if (!isObject(response)) {
354
+ throw new SeiMcpError(`${context} 响应不是 JSON 对象`, 'INVALID_REQUEST');
355
+ }
356
+ if (!allowFailure && isSeiFailure(response)) {
357
+ throw new SeiMcpError(`${context} 失败:${String(response.message ?? response.msg ?? response.code ?? '未知错误')}`, 'INVALID_REQUEST', {
358
+ response: safeResponse(response),
359
+ });
360
+ }
361
+ }
362
+ function extractToken(response) {
363
+ validateSeiEnvelope(response, 'login', false, false);
364
+ if (!isObject(response) || !isObject(response.data) || typeof response.data.token !== 'string' || !response.data.token.trim()) {
365
+ throw new SeiMcpError('登录响应中没有 data.token', 'INVALID_REQUEST');
366
+ }
367
+ return response.data.token.trim();
368
+ }
369
+ function extractResponseRows(response) {
370
+ if (!isObject(response)) {
371
+ return [];
372
+ }
373
+ const data = response.data;
374
+ if (Array.isArray(data)) {
375
+ return data;
376
+ }
377
+ if (isObject(data)) {
378
+ for (const key of ['rows', 'records', 'list', 'data']) {
379
+ const value = data[key];
380
+ if (Array.isArray(value)) {
381
+ return value;
382
+ }
383
+ if (typeof value === 'string' && value.trim()) {
384
+ try {
385
+ const parsed = JSON.parse(value);
386
+ if (Array.isArray(parsed)) {
387
+ return parsed;
388
+ }
389
+ }
390
+ catch {
391
+ return [];
392
+ }
393
+ }
394
+ }
395
+ }
396
+ return [];
397
+ }
398
+ function resolveAccountProject(row, requestedProject) {
399
+ const normalizedProject = toText(requestedProject);
400
+ if (normalizedProject) {
401
+ return normalizedProject;
402
+ }
403
+ const sysid = toText(getRowField(row, '_SYSID', 'sysid'));
404
+ if (!sysid) {
405
+ return ACCOUNT_LOGIN_DEFAULT_PROJECT;
406
+ }
407
+ return sysid.split(',', 1)[0]?.trim().replace(/^['"]|['"]$/g, '') || ACCOUNT_LOGIN_DEFAULT_PROJECT;
408
+ }
409
+ function getRowField(row, ...names) {
410
+ for (const name of names) {
411
+ if (name in row) {
412
+ return row[name];
413
+ }
414
+ }
415
+ const lowered = new Map(Object.entries(row).map(([key, value]) => [key.toLowerCase(), value]));
416
+ for (const name of names) {
417
+ if (lowered.has(name.toLowerCase())) {
418
+ return lowered.get(name.toLowerCase());
419
+ }
420
+ }
421
+ return undefined;
422
+ }
423
+ async function loadCachedToken(settings) {
424
+ const path = getTokenCachePath(settings);
425
+ try {
426
+ const raw = await readFile(path, 'utf8');
427
+ const data = JSON.parse(raw);
428
+ if (data.version !== TOKEN_CACHE_VERSION) {
429
+ return null;
430
+ }
431
+ if (data.baseUrl !== normalizeBaseUrl(settings.baseUrl)) {
432
+ return null;
433
+ }
434
+ if ((toText(data.role) || null) !== (settings.account ? null : settings.role || null)) {
435
+ return null;
436
+ }
437
+ if ((toText(data.account) || null) !== (settings.account || null)) {
438
+ return null;
439
+ }
440
+ return typeof data.token === 'string' && data.token.trim() ? data.token.trim() : null;
441
+ }
442
+ catch {
443
+ return null;
444
+ }
445
+ }
446
+ async function saveCachedToken(settings, token, logger) {
447
+ const path = getTokenCachePath(settings);
448
+ try {
449
+ await mkdir(dirname(path), { recursive: true });
450
+ await writeFile(path, JSON.stringify({
451
+ version: TOKEN_CACHE_VERSION,
452
+ baseUrl: normalizeBaseUrl(settings.baseUrl),
453
+ role: settings.account ? null : settings.role || null,
454
+ account: settings.account || null,
455
+ token: token.trim(),
456
+ }, null, 2) + '\n', 'utf8');
457
+ await chmod(path, 0o600).catch(() => undefined);
458
+ }
459
+ catch (error) {
460
+ logger.warn('写入 token 缓存失败', {
461
+ cause: error instanceof Error ? error.message : String(error),
462
+ });
463
+ }
464
+ }
465
+ function getTokenCachePath(settings) {
466
+ const identity = settings.account ? `account:${settings.account}` : `role:${settings.role || '__default__'}`;
467
+ const scope = `${normalizeBaseUrl(settings.baseUrl)}\0${identity}`;
468
+ const hash = createHash('sha256').update(scope).digest('hex').slice(0, 16);
469
+ return resolve(getTokenCacheDir(), `${TOKEN_CACHE_FILE_PREFIX}${hash}${TOKEN_CACHE_FILE_SUFFIX}`);
470
+ }
471
+ function getTokenCacheDir() {
472
+ const xdg = toText(process.env.XDG_CACHE_HOME);
473
+ if (xdg) {
474
+ return resolve(xdg, TOKEN_CACHE_DIR_NAME);
475
+ }
476
+ return resolve(homedir(), '.cache', TOKEN_CACHE_DIR_NAME);
477
+ }
478
+ function escapeSqlLiteral(value) {
479
+ return String(value).replace(/'/g, "''");
480
+ }
481
+ function isUnauthenticatedResponse(response) {
482
+ if (!isObject(response)) {
483
+ return false;
484
+ }
485
+ return Number(response.code) === -2;
486
+ }
487
+ function isSeiFailure(response) {
488
+ if (!isObject(response)) {
489
+ return false;
490
+ }
491
+ if (response.success === false) {
492
+ return true;
493
+ }
494
+ const code = Number(response.code);
495
+ return Number.isFinite(code) && code < 1;
496
+ }
497
+ function safeResponse(response) {
498
+ if (!isObject(response)) {
499
+ return response;
500
+ }
501
+ return {
502
+ code: response.code,
503
+ success: response.success,
504
+ message: response.message,
505
+ };
506
+ }
507
+ function isObject(value) {
508
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
509
+ }
510
+ function normalizeTimeout(value) {
511
+ const numeric = typeof value === 'number' ? value : Number(value);
512
+ if (Number.isFinite(numeric) && numeric > 0) {
513
+ return numeric < 1000 ? Math.round(numeric * 1000) : Math.round(numeric);
514
+ }
515
+ const env = Number(process.env.SEI_TIMEOUT ?? '');
516
+ if (Number.isFinite(env) && env > 0) {
517
+ return env < 1000 ? Math.round(env * 1000) : Math.round(env);
518
+ }
519
+ return DEFAULT_TIMEOUT_MS;
520
+ }
521
+ function resolveRole(role, fallback) {
522
+ return firstText(role, fallback, process.env.SEI_AI_LOGIN_ROLE).toLowerCase();
523
+ }
524
+ function firstText(...values) {
525
+ for (const value of values) {
526
+ const text = toText(value);
527
+ if (text) {
528
+ return text;
529
+ }
530
+ }
531
+ return '';
532
+ }
533
+ function toText(value) {
534
+ return typeof value === 'string' ? value.trim() : '';
535
+ }
@@ -0,0 +1,139 @@
1
+ import type { JsonValue } from './utils.js';
2
+ export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
3
+ export interface Logger {
4
+ debug(message: string, meta?: JsonValue): void;
5
+ info(message: string, meta?: JsonValue): void;
6
+ warn(message: string, meta?: JsonValue): void;
7
+ error(message: string, meta?: JsonValue): void;
8
+ }
9
+ export interface SeiMcpConfig {
10
+ baseUrl: string;
11
+ auth: {
12
+ mode: 'ai-login' | 'token';
13
+ token: string;
14
+ tokenEnv: string;
15
+ aiKey: string;
16
+ aiKeyEnv: string;
17
+ account: string;
18
+ accountEnv: string;
19
+ role: string;
20
+ roleEnv: string;
21
+ };
22
+ tools: {
23
+ query: boolean;
24
+ save: boolean;
25
+ schema: boolean;
26
+ querySql: boolean;
27
+ };
28
+ targets: {
29
+ modules: Record<string, TargetConfig>;
30
+ tables: Record<string, TargetConfig>;
31
+ sources: Record<string, TargetConfig>;
32
+ views: Record<string, TargetConfig>;
33
+ };
34
+ }
35
+ export type RuntimeTransport = 'stdio' | 'streamable-http';
36
+ export type RuntimeCommand = 'serve' | 'cli' | 'help';
37
+ export interface HttpRuntimeOptions {
38
+ command: RuntimeCommand;
39
+ transport: RuntimeTransport;
40
+ host: string;
41
+ port: number;
42
+ path: string;
43
+ enableJsonResponse: boolean;
44
+ cliArgs: string[];
45
+ }
46
+ export interface HttpServerHandle {
47
+ url: URL;
48
+ close(): Promise<void>;
49
+ }
50
+ export interface TargetConfig {
51
+ mainTable?: string;
52
+ query?: {
53
+ fields?: string[];
54
+ defaultFields?: string[];
55
+ allowAllFields?: boolean;
56
+ };
57
+ save?: {
58
+ actions?: string[];
59
+ fields?: string[];
60
+ };
61
+ }
62
+ export interface AuthHeaders {
63
+ token: string;
64
+ authorization?: string;
65
+ }
66
+ export interface QueryToolInput {
67
+ head: Record<string, string>;
68
+ option?: {
69
+ fields?: string;
70
+ response_format?: 'markdown' | 'json';
71
+ [key: string]: unknown;
72
+ };
73
+ }
74
+ export interface SaveToolInput {
75
+ head: Record<string, string>;
76
+ data: Array<{
77
+ action: string;
78
+ rows?: Array<{
79
+ keyVal?: unknown;
80
+ row?: Record<string, unknown>;
81
+ }>;
82
+ }>;
83
+ query?: Record<string, unknown>;
84
+ }
85
+ export interface QuerySqlToolInput {
86
+ sql: string;
87
+ page?: number;
88
+ size?: number;
89
+ response_format?: 'markdown' | 'json';
90
+ }
91
+ export interface SeiAuthSettings {
92
+ baseUrl: string;
93
+ mode: 'ai-login' | 'token';
94
+ token: string;
95
+ aiKey: string;
96
+ role: string;
97
+ account: string;
98
+ accountProject: string;
99
+ accountCheckcode: string;
100
+ timeoutMs: number;
101
+ }
102
+ export interface SeiCommandOptions {
103
+ baseUrl?: string;
104
+ token?: string;
105
+ aiKey?: string;
106
+ role?: string;
107
+ account?: string;
108
+ accountProject?: string;
109
+ accountCheckcode?: string;
110
+ timeoutMs?: number;
111
+ noToken?: boolean;
112
+ allowFailure?: boolean;
113
+ allowEmpty?: boolean;
114
+ }
115
+ export interface SeiRequestOptions {
116
+ token?: string | null;
117
+ timeoutMs?: number;
118
+ extraHeaders?: Record<string, string>;
119
+ }
120
+ export interface SeiResponseValidationOptions {
121
+ context: string;
122
+ allowFailure?: boolean;
123
+ allowEmpty?: boolean;
124
+ }
125
+ export interface ToolRuntime {
126
+ config: SeiMcpConfig;
127
+ client: SeiClient;
128
+ logger: Logger;
129
+ }
130
+ export interface SeiClient {
131
+ query(payload: Record<string, unknown>): Promise<unknown>;
132
+ save(payload: Record<string, unknown>): Promise<unknown>;
133
+ querySql(payload: {
134
+ sql: string;
135
+ page: number;
136
+ size: number;
137
+ }): Promise<unknown>;
138
+ describeTable(tableName: string): Promise<unknown>;
139
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
2
+ [key: string]: JsonValue;
3
+ };
4
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
5
+ export declare function toTrimmedString(value: unknown): string;
6
+ export declare function stringifySafe(value: unknown): string;
7
+ export declare function truncateText(value: unknown, limit: number): string;
8
+ export declare function uniqueStrings(values: Iterable<string>): string[];
9
+ export declare function normalizeBaseUrl(value: unknown): string;
10
+ export declare function normalizePath(value: unknown): string;
11
+ export declare function makeUrl(baseUrl: string, path: string): string;
12
+ export declare function toPositiveInteger(value: unknown, fallback: number): number;