@myassis/gateway 1.0.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/README.md +194 -0
- package/dist/.env +6 -0
- package/dist/api/index.js +182 -0
- package/dist/config/index.js +41 -0
- package/dist/index.js +183 -0
- package/dist/middleware/auth.js +53 -0
- package/dist/middleware/errorHandler.js +20 -0
- package/dist/routes/agent.js +513 -0
- package/dist/routes/auth.js +172 -0
- package/dist/routes/chat.js +45 -0
- package/dist/routes/config.js +21 -0
- package/dist/routes/models.js +123 -0
- package/dist/routes/service.js +240 -0
- package/dist/routes/settings.js +101 -0
- package/dist/routes/skillHub.js +126 -0
- package/dist/routes/skills.js +159 -0
- package/dist/routes/tasks.js +149 -0
- package/dist/routes/upload.js +129 -0
- package/dist/routes/version.js +66 -0
- package/dist/services/HMSPushService.js +24 -0
- package/dist/services/LocalTaskService.js +223 -0
- package/dist/services/NotificationService.js +242 -0
- package/dist/services/ServiceManager.js +348 -0
- package/dist/services/TaskSchedulerService.js +195 -0
- package/dist/services/TaskService.js +240 -0
- package/dist/services/WebSocketService.js +236 -0
- package/dist/services/agent/Agent.js +120 -0
- package/dist/services/agent/AgentManager.js +265 -0
- package/dist/services/agent/AgentStore.js +73 -0
- package/dist/services/dataService.js +293 -0
- package/dist/services/index.js +15 -0
- package/dist/services/llm/LLMClient.js +724 -0
- package/dist/services/memory/MemoryManager.js +117 -0
- package/dist/services/model/ModelCapabilities.js +141 -0
- package/dist/services/model/index.js +4 -0
- package/dist/services/models.js +16 -0
- package/dist/services/session/MigrationManager.js +176 -0
- package/dist/services/session/Session.js +733 -0
- package/dist/services/session/SessionManager.js +255 -0
- package/dist/services/session/SessionStore.js +186 -0
- package/dist/services/session/index.js +3 -0
- package/dist/services/skills.js +34 -0
- package/dist/services/systemPrompt.js +150 -0
- package/dist/services/task/PushTokenStore.js +124 -0
- package/dist/services/task/TaskStore.js +143 -0
- package/dist/services/tools/calculator.js +27 -0
- package/dist/services/tools/edit.js +318 -0
- package/dist/services/tools/exec.js +119 -0
- package/dist/services/tools/fetch.js +155 -0
- package/dist/services/tools/file.js +315 -0
- package/dist/services/tools/index.js +48 -0
- package/dist/services/tools/keyboard.js +145 -0
- package/dist/services/tools/model.js +86 -0
- package/dist/services/tools/mouse.js +55 -0
- package/dist/services/tools/screenshot.js +19 -0
- package/dist/services/tools/search.js +53 -0
- package/dist/services/tools/skill.js +108 -0
- package/dist/services/tools/task.js +110 -0
- package/dist/services/tools/types.js +1 -0
- package/dist/services/tools/webFetch.js +34 -0
- package/dist/stores/authStore.js +178 -0
- package/dist/stores/index.js +6 -0
- package/dist/stores/memoryStore.js +191 -0
- package/dist/stores/persistStore.js +317 -0
- package/package.json +94 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { getLogger } from '@pocketclaw/shared';
|
|
2
|
+
const logger = getLogger('MemoryManager');
|
|
3
|
+
import { toModel } from '../models';
|
|
4
|
+
import { modelsService } from '../dataService';
|
|
5
|
+
import { LLMClient } from '../llm/LLMClient';
|
|
6
|
+
import { appConfig } from '@/config';
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
summaryThreshold: 10,
|
|
9
|
+
enabled: true,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* 记忆管理器
|
|
13
|
+
* 实现总结式记忆:当对话超过一定长度时,自动生成对话摘要
|
|
14
|
+
*/
|
|
15
|
+
export class MemoryManager {
|
|
16
|
+
session;
|
|
17
|
+
signal;
|
|
18
|
+
config = DEFAULT_CONFIG;
|
|
19
|
+
constructor(session, signal) {
|
|
20
|
+
this.session = session;
|
|
21
|
+
this.signal = signal;
|
|
22
|
+
}
|
|
23
|
+
toSummaryMessage(summary, createdAt, modelName) {
|
|
24
|
+
return {
|
|
25
|
+
sessionId: this.session.id,
|
|
26
|
+
content: `对话内容摘要:\n${summary}`,
|
|
27
|
+
role: 'system',
|
|
28
|
+
id: `summary_${createdAt}`,
|
|
29
|
+
createdAt,
|
|
30
|
+
modelName,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async getHistoryMessagesAsync() {
|
|
34
|
+
// 1. 获取所有消息
|
|
35
|
+
const messages = this.session.getMessages();
|
|
36
|
+
// 2. 消息少于摘要配置的阈值数,直接返回
|
|
37
|
+
logger.debug('message length:', messages.length);
|
|
38
|
+
if (messages.length < this.config.summaryThreshold) {
|
|
39
|
+
return messages;
|
|
40
|
+
}
|
|
41
|
+
// 3. 检查是否有摘要
|
|
42
|
+
const hasSummary = this.session.lastMessageSummary;
|
|
43
|
+
logger.debug('hasSummary', hasSummary);
|
|
44
|
+
if (!hasSummary) {
|
|
45
|
+
// 4A. 首次摘要生成
|
|
46
|
+
const summary = await this.generateSummaryAsync(messages.slice(0, -appConfig.messageKeep), null);
|
|
47
|
+
this.session.lastMessageSummary = summary;
|
|
48
|
+
this.session.lastMessageSummaryAt = Date.now();
|
|
49
|
+
this.session.save();
|
|
50
|
+
return [
|
|
51
|
+
this.toSummaryMessage(summary, this.session.lastMessageSummaryAt, ''),
|
|
52
|
+
...messages.slice(-appConfig.messageKeep),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// 4B. 增量摘要更新
|
|
57
|
+
const newMessages = messages.filter((m) => m.createdAt > this.session.lastMessageSummaryAt);
|
|
58
|
+
logger.debug('new messages length:', newMessages.length);
|
|
59
|
+
if (newMessages.length < this.config.summaryThreshold) {
|
|
60
|
+
// 新消息少于阈值条:返回摘要 + 从摘要时刻起的所有消息
|
|
61
|
+
const fromSummary = messages.filter((m) => m.createdAt >= this.session.lastMessageSummaryAt);
|
|
62
|
+
return [
|
|
63
|
+
this.toSummaryMessage(this.session.lastMessageSummary, this.session.lastMessageSummaryAt, ''),
|
|
64
|
+
...fromSummary,
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// 新消息超过阈值条,重新生成摘要
|
|
69
|
+
const summary = await this.generateSummaryAsync(newMessages.slice(0, -appConfig.messageKeep), this.session.lastMessageSummary);
|
|
70
|
+
this.session.lastMessageSummary = summary;
|
|
71
|
+
this.session.lastMessageSummaryAt = Date.now();
|
|
72
|
+
this.session.save();
|
|
73
|
+
return [
|
|
74
|
+
this.toSummaryMessage(summary, this.session.lastMessageSummaryAt, ''),
|
|
75
|
+
...messages.slice(-appConfig.messageKeep),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 生成摘要提示词
|
|
82
|
+
*/
|
|
83
|
+
buildSummaryPrompt(messages, lastSummary) {
|
|
84
|
+
const conversation = messages
|
|
85
|
+
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
|
86
|
+
.map((m) => `${m.role === 'user' ? '用户' : '助手'}: ${m.content}${m.toolCalls ? JSON.stringify(m.toolCalls) : ''}`)
|
|
87
|
+
.join('\n');
|
|
88
|
+
return `请分析以下对话内容,生成一个简洁的摘要,保留关键信息、错误信息和上下文。
|
|
89
|
+
|
|
90
|
+
对话内容:
|
|
91
|
+
${conversation}
|
|
92
|
+
|
|
93
|
+
${lastSummary ? '上一次内容摘要:\n' + lastSummary : ''}
|
|
94
|
+
|
|
95
|
+
请用以下格式返回摘要(只返回摘要,不要其他内容):
|
|
96
|
+
【摘要】:简要概括对话主题和关键信息(不超过100字)
|
|
97
|
+
【要点】:列出2-5个关键要点
|
|
98
|
+
【错误】:列出犯过的错误`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 生成对话摘要
|
|
102
|
+
*/
|
|
103
|
+
async generateSummaryAsync(messages, lastSummary) {
|
|
104
|
+
const summaryPrompt = this.buildSummaryPrompt(messages, lastSummary);
|
|
105
|
+
const llmClient = new LLMClient((await modelsService.list()).data.map((x) => toModel(x)), [{ role: 'user', content: summaryPrompt }], this.signal, []);
|
|
106
|
+
const response = await llmClient.Chat();
|
|
107
|
+
const content = (response.content || '').trim();
|
|
108
|
+
if (!content) {
|
|
109
|
+
logger.warn('摘要内容为空,保留旧摘要', {
|
|
110
|
+
lastSummary,
|
|
111
|
+
responseKeys: Object.keys(response),
|
|
112
|
+
});
|
|
113
|
+
return lastSummary || '(摘要生成失败,内容为空)';
|
|
114
|
+
}
|
|
115
|
+
return content;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型能力配置
|
|
3
|
+
* 定义各模型擅长领域和优先级
|
|
4
|
+
*/
|
|
5
|
+
export const MODEL_CAPABILITIES = {
|
|
6
|
+
// Anthropic 系列
|
|
7
|
+
'claude-3-5-sonnet': {
|
|
8
|
+
strengths: ['reasoning', 'coding', 'analysis', 'writing'],
|
|
9
|
+
priority: 9,
|
|
10
|
+
preferLongContext: true,
|
|
11
|
+
},
|
|
12
|
+
'claude-3-opus': {
|
|
13
|
+
strengths: ['reasoning', 'coding', 'analysis', 'writing'],
|
|
14
|
+
priority: 10,
|
|
15
|
+
preferLongContext: true,
|
|
16
|
+
},
|
|
17
|
+
'claude-3-haiku': {
|
|
18
|
+
strengths: ['quick', 'simple', 'text'],
|
|
19
|
+
priority: 5,
|
|
20
|
+
},
|
|
21
|
+
// OpenAI 系列
|
|
22
|
+
'gpt-4o': {
|
|
23
|
+
strengths: ['reasoning', 'coding', 'vision', 'creative'],
|
|
24
|
+
priority: 8,
|
|
25
|
+
preferLongContext: true,
|
|
26
|
+
},
|
|
27
|
+
'gpt-4-turbo': {
|
|
28
|
+
strengths: ['reasoning', 'coding', 'vision'],
|
|
29
|
+
priority: 8,
|
|
30
|
+
},
|
|
31
|
+
'gpt-4': {
|
|
32
|
+
strengths: ['reasoning', 'coding', 'analysis'],
|
|
33
|
+
priority: 7,
|
|
34
|
+
},
|
|
35
|
+
'gpt-3.5-turbo': {
|
|
36
|
+
strengths: ['quick', 'simple', 'text'],
|
|
37
|
+
priority: 4,
|
|
38
|
+
},
|
|
39
|
+
// DeepSeek 系列
|
|
40
|
+
'deepseek-chat': {
|
|
41
|
+
strengths: ['coding', 'math', 'reasoning', 'text'],
|
|
42
|
+
priority: 7,
|
|
43
|
+
},
|
|
44
|
+
'deepseek-coder': {
|
|
45
|
+
strengths: ['coding', 'code-review'],
|
|
46
|
+
priority: 8,
|
|
47
|
+
},
|
|
48
|
+
// 字节豆包系列
|
|
49
|
+
'doubao-pro': {
|
|
50
|
+
strengths: ['text', 'chinese', 'analysis'],
|
|
51
|
+
priority: 6,
|
|
52
|
+
},
|
|
53
|
+
'doubao-lite': {
|
|
54
|
+
strengths: ['quick', 'simple', 'chinese'],
|
|
55
|
+
priority: 4,
|
|
56
|
+
},
|
|
57
|
+
// 通义千问
|
|
58
|
+
'qwen-plus': {
|
|
59
|
+
strengths: ['text', 'chinese', 'coding'],
|
|
60
|
+
priority: 6,
|
|
61
|
+
},
|
|
62
|
+
'qwen-turbo': {
|
|
63
|
+
strengths: ['quick', 'simple', 'chinese'],
|
|
64
|
+
priority: 4,
|
|
65
|
+
},
|
|
66
|
+
// Kimi
|
|
67
|
+
'kimi-plus': {
|
|
68
|
+
strengths: ['long-context', 'text', 'analysis'],
|
|
69
|
+
priority: 7,
|
|
70
|
+
preferLongContext: true,
|
|
71
|
+
},
|
|
72
|
+
'kimi-turbo': {
|
|
73
|
+
strengths: ['text', 'quick'],
|
|
74
|
+
priority: 5,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* 内容分析标签定义
|
|
79
|
+
*/
|
|
80
|
+
export const CAPABILITY_KEYWORDS = {
|
|
81
|
+
// 编程相关
|
|
82
|
+
coding: [
|
|
83
|
+
'code', '编程', '代码', '函数', 'algorithm', 'python', 'javascript',
|
|
84
|
+
'typescript', 'java', 'bug', 'debug', 'api', 'git', 'sql', 'regex',
|
|
85
|
+
'class', 'interface', 'module', 'import', 'export', 'compile', 'debug'
|
|
86
|
+
],
|
|
87
|
+
// 数学相关
|
|
88
|
+
math: [
|
|
89
|
+
'math', '数学', '计算', 'equation', '公式', '求导', '积分', '矩阵',
|
|
90
|
+
'概率', '统计', 'algorithm', '算法', '数值', 'algebra'
|
|
91
|
+
],
|
|
92
|
+
// 推理分析
|
|
93
|
+
reasoning: [
|
|
94
|
+
'reasoning', '分析', '推理', '逻辑', '思考', '为什么', '原因',
|
|
95
|
+
'分析', 'evaluate', 'compare', 'difference', 'explain'
|
|
96
|
+
],
|
|
97
|
+
// 创意写作
|
|
98
|
+
creative: [
|
|
99
|
+
'创意', 'write', 'story', 'poem', '小说', '文章', '写作',
|
|
100
|
+
'create', '设计', 'imagine', ' brainstorm'
|
|
101
|
+
],
|
|
102
|
+
// 视觉/图像
|
|
103
|
+
vision: [
|
|
104
|
+
'image', '图片', '图像', 'vision', '识别', 'ocr', '图表',
|
|
105
|
+
'vision', 'visual', 'photo'
|
|
106
|
+
],
|
|
107
|
+
// 长上下文
|
|
108
|
+
'long-context': [
|
|
109
|
+
'长文', '总结', '摘要', '文档', '合同', '论文', '报告',
|
|
110
|
+
'全文', '这篇文章', '这本书', '详细'
|
|
111
|
+
],
|
|
112
|
+
// 中文相关
|
|
113
|
+
chinese: [
|
|
114
|
+
'中文', ' chinese', '中国', '汉字', '文言文', '古诗'
|
|
115
|
+
],
|
|
116
|
+
// 快速响应
|
|
117
|
+
quick: [
|
|
118
|
+
'quick', 'quickly', '简单', '快', ' brief', '简短'
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* 获取模型的默认配置
|
|
123
|
+
*/
|
|
124
|
+
export function getModelCapabilityConfig(modelId) {
|
|
125
|
+
// 尝试精确匹配
|
|
126
|
+
if (MODEL_CAPABILITIES[modelId]) {
|
|
127
|
+
return MODEL_CAPABILITIES[modelId];
|
|
128
|
+
}
|
|
129
|
+
// 尝试前缀匹配
|
|
130
|
+
const prefix = modelId.split('-').slice(0, 2).join('-');
|
|
131
|
+
for (const key of Object.keys(MODEL_CAPABILITIES)) {
|
|
132
|
+
if (key.startsWith(prefix)) {
|
|
133
|
+
return MODEL_CAPABILITIES[key];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// 默认配置
|
|
137
|
+
return {
|
|
138
|
+
strengths: ['text'],
|
|
139
|
+
priority: 5,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function toModel(m) {
|
|
2
|
+
return {
|
|
3
|
+
id: String(m.id),
|
|
4
|
+
modelName: m.modelName,
|
|
5
|
+
modelId: m.modelId,
|
|
6
|
+
provider: m.provider,
|
|
7
|
+
type: m.type || 'chat',
|
|
8
|
+
description: m.description || '',
|
|
9
|
+
capabilities: m.capabilities || [],
|
|
10
|
+
maxTokens: m.maxTokens || 0,
|
|
11
|
+
isActive: m.isActive,
|
|
12
|
+
baseUrl: m.baseUrl,
|
|
13
|
+
score: m.score || 0,
|
|
14
|
+
isPrimary: m.isPrimary
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { getLogger } from '@pocketclaw/shared';
|
|
4
|
+
const logger = getLogger('MigrationManager');
|
|
5
|
+
export class MigrationManager {
|
|
6
|
+
db;
|
|
7
|
+
migrationsDir;
|
|
8
|
+
constructor(db, migrationsDir) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
this.migrationsDir = migrationsDir;
|
|
11
|
+
this.initMigrationTable();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 初始化迁移记录表
|
|
15
|
+
*/
|
|
16
|
+
initMigrationTable() {
|
|
17
|
+
this.db.exec(`
|
|
18
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
19
|
+
version INTEGER PRIMARY KEY,
|
|
20
|
+
name TEXT NOT NULL,
|
|
21
|
+
applied_at INTEGER NOT NULL
|
|
22
|
+
);
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 获取当前数据库版本
|
|
27
|
+
*/
|
|
28
|
+
getCurrentVersion() {
|
|
29
|
+
const row = this.db.prepare('SELECT MAX(version) as version FROM schema_migrations').get();
|
|
30
|
+
return row?.version ?? 0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 获取所有已应用的迁移
|
|
34
|
+
*/
|
|
35
|
+
getAppliedMigrations() {
|
|
36
|
+
return this.db.prepare('SELECT * FROM schema_migrations ORDER BY version').all();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 执行数据库迁移
|
|
40
|
+
*/
|
|
41
|
+
migrate() {
|
|
42
|
+
const currentVersion = this.getCurrentVersion();
|
|
43
|
+
const appliedMigrations = this.getAppliedMigrations();
|
|
44
|
+
const appliedNames = new Set(appliedMigrations.map(m => m.name));
|
|
45
|
+
// 读取迁移目录下的所有 SQL 文件
|
|
46
|
+
if (!fs.existsSync(this.migrationsDir)) {
|
|
47
|
+
logger.info(`Migrations directory does not exist: ${this.migrationsDir}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const migrationFiles = fs.readdirSync(this.migrationsDir)
|
|
51
|
+
.filter(file => file.endsWith('.sql'))
|
|
52
|
+
.sort(); // 按文件名排序
|
|
53
|
+
let applied = 0;
|
|
54
|
+
for (const file of migrationFiles) {
|
|
55
|
+
// 从文件名提取版本号和名称
|
|
56
|
+
// 格式: XXX_description.sql 例如: 001_create_sessions_table.sql
|
|
57
|
+
const match = file.match(/^(\d+)_(.+)\.sql$/);
|
|
58
|
+
if (!match) {
|
|
59
|
+
logger.warn(`Skipping invalid migration file: ${file}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const version = parseInt(match[1], 10);
|
|
63
|
+
const name = match[2];
|
|
64
|
+
// 如果已应用,跳过
|
|
65
|
+
if (appliedNames.has(name)) {
|
|
66
|
+
logger.debug(`Skipping already applied migration: ${name}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// 如果版本低于当前版本,报错(防止降级迁移)
|
|
70
|
+
if (version <= currentVersion) {
|
|
71
|
+
logger.error(`Migration ${name} has version ${version} but current version is ${currentVersion}. Cannot apply older migration.`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// 读取并执行 SQL 文件
|
|
75
|
+
const filePath = path.join(this.migrationsDir, file);
|
|
76
|
+
const sql = fs.readFileSync(filePath, 'utf-8');
|
|
77
|
+
logger.info(`Applying migration: ${name} (version: ${version})`);
|
|
78
|
+
try {
|
|
79
|
+
// 分割多条 SQL 语句并执行
|
|
80
|
+
const statements = this.splitSqlStatements(sql);
|
|
81
|
+
for (const statement of statements) {
|
|
82
|
+
if (statement.trim() && !statement.trim().startsWith('SELECT')) {
|
|
83
|
+
try {
|
|
84
|
+
this.db.exec(statement);
|
|
85
|
+
}
|
|
86
|
+
catch (execError) {
|
|
87
|
+
// 忽略 "duplicate column name" 错误(列已存在)
|
|
88
|
+
if (execError.code !== 'SQLITE_ERROR' || !execError.message?.includes('duplicate column name')) {
|
|
89
|
+
throw execError;
|
|
90
|
+
}
|
|
91
|
+
logger.warn(`Column already exists, skipping: ${statement.substring(0, 50)}...`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 记录迁移
|
|
96
|
+
this.db.prepare('INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)').run(version, name, Date.now());
|
|
97
|
+
logger.info(`Successfully applied migration: ${name}`);
|
|
98
|
+
applied++;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
logger.error(`Failed to apply migration ${name}: ${error}`);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (applied === 0) {
|
|
106
|
+
logger.info('Database is up to date');
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
logger.info(`Applied ${applied} migration(s)`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 分割 SQL 语句(支持 ; 分隔)
|
|
114
|
+
* 忽略 SQL 注释
|
|
115
|
+
*/
|
|
116
|
+
splitSqlStatements(sql) {
|
|
117
|
+
const lines = sql.split('\n');
|
|
118
|
+
const statements = [];
|
|
119
|
+
let currentStatement = '';
|
|
120
|
+
let inBlockComment = false;
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const trimmed = line.trim();
|
|
123
|
+
// 跳过空行和单行注释
|
|
124
|
+
if (!trimmed || trimmed.startsWith('--')) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// 处理块注释
|
|
128
|
+
if (inBlockComment) {
|
|
129
|
+
if (trimmed.includes('*/')) {
|
|
130
|
+
inBlockComment = false;
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (trimmed.startsWith('/*')) {
|
|
135
|
+
inBlockComment = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
currentStatement += ' ' + trimmed;
|
|
139
|
+
// 检查是否以分号结尾
|
|
140
|
+
if (currentStatement.trim().endsWith(';')) {
|
|
141
|
+
statements.push(currentStatement.trim());
|
|
142
|
+
currentStatement = '';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 处理最后一条语句(可能没有分号)
|
|
146
|
+
if (currentStatement.trim()) {
|
|
147
|
+
statements.push(currentStatement.trim());
|
|
148
|
+
}
|
|
149
|
+
return statements;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 获取迁移状态
|
|
153
|
+
*/
|
|
154
|
+
getStatus() {
|
|
155
|
+
const currentVersion = this.getCurrentVersion();
|
|
156
|
+
const appliedMigrations = this.getAppliedMigrations();
|
|
157
|
+
if (!fs.existsSync(this.migrationsDir)) {
|
|
158
|
+
return { currentVersion, applied: appliedMigrations, pending: [], error: 'Migrations directory not found' };
|
|
159
|
+
}
|
|
160
|
+
const migrationFiles = fs.readdirSync(this.migrationsDir)
|
|
161
|
+
.filter(file => file.endsWith('.sql'))
|
|
162
|
+
.sort();
|
|
163
|
+
const pending = [];
|
|
164
|
+
for (const file of migrationFiles) {
|
|
165
|
+
const match = file.match(/^(\d+)_(.+)\.sql$/);
|
|
166
|
+
if (match) {
|
|
167
|
+
const name = match[2];
|
|
168
|
+
const applied = appliedMigrations.some(m => m.name === name);
|
|
169
|
+
if (!applied) {
|
|
170
|
+
pending.push(file);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { currentVersion, applied: appliedMigrations, pending };
|
|
175
|
+
}
|
|
176
|
+
}
|