@axhub/genie 0.1.6 → 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.
- package/dist/api-docs.html +351 -909
- package/dist/assets/index-CVjMty4a.js +902 -0
- package/dist/assets/index-eo5scY_Z.css +32 -0
- package/dist/index.html +5 -5
- package/dist/manifest.json +2 -2
- package/package.json +8 -2
- package/server/channels/core/ChannelManager.js +399 -0
- package/server/channels/core/PluginManager.js +59 -0
- package/server/channels/index.js +3 -0
- package/server/channels/plugins/BasePlugin.js +46 -0
- package/server/channels/plugins/dingtalk/DingTalkAdapter.js +156 -0
- package/server/channels/plugins/dingtalk/DingTalkPlugin.js +592 -0
- package/server/channels/plugins/dingtalk/index.js +2 -0
- package/server/channels/plugins/lark/LarkAdapter.js +100 -0
- package/server/channels/plugins/lark/LarkCards.js +43 -0
- package/server/channels/plugins/lark/LarkPlugin.js +260 -0
- package/server/channels/runtime/AgentRuntimeAdapter.js +179 -0
- package/server/channels/runtime/DingTalkStreamWriter.js +105 -0
- package/server/channels/runtime/LarkStreamWriter.js +99 -0
- package/server/channels/store/ChannelStore.js +236 -0
- package/server/database/db.js +109 -1
- package/server/database/init.sql +47 -1
- package/server/gemini-cli.js +280 -0
- package/server/index.js +230 -11
- package/server/openai-codex.js +104 -8
- package/server/opencode-cli.js +673 -0
- package/server/projects.js +645 -5
- package/server/routes/agent.js +40 -12
- package/server/routes/channels.js +221 -0
- package/server/routes/cli-auth.js +317 -0
- package/server/routes/commands.js +29 -3
- package/server/routes/git.js +15 -5
- package/server/routes/opencode.js +72 -0
- package/shared/modelConstants.js +62 -17
- package/dist/assets/index-CtRxrKDm.css +0 -32
- package/dist/assets/index-OENtErNy.js +0 -1249
- package/server/database/auth.db +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
import { db } from '../../database/db.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIGS = {
|
|
6
|
+
lark: {
|
|
7
|
+
appId: '',
|
|
8
|
+
appSecret: '',
|
|
9
|
+
encryptKey: '',
|
|
10
|
+
verificationToken: '',
|
|
11
|
+
defaultBackend: 'codex',
|
|
12
|
+
defaultModel: '',
|
|
13
|
+
defaultProjectPath: '',
|
|
14
|
+
},
|
|
15
|
+
dingtalk: {
|
|
16
|
+
clientId: '',
|
|
17
|
+
clientSecret: '',
|
|
18
|
+
defaultBackend: 'codex',
|
|
19
|
+
defaultModel: '',
|
|
20
|
+
defaultProjectPath: '',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const PLATFORM_TO_PLUGIN_ID = {
|
|
25
|
+
lark: 'lark_default',
|
|
26
|
+
dingtalk: 'dingtalk_default',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const PLUGIN_ID_TO_PLATFORM = {
|
|
30
|
+
lark_default: 'lark',
|
|
31
|
+
dingtalk_default: 'dingtalk',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function now() {
|
|
35
|
+
return new Date().toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizePlatform(platform) {
|
|
39
|
+
const normalized = String(platform || '').trim().toLowerCase();
|
|
40
|
+
return normalized === 'dingtalk' ? 'dingtalk' : 'lark';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolvePlatformByPluginId(pluginId) {
|
|
44
|
+
return PLUGIN_ID_TO_PLATFORM[pluginId] || 'lark';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolvePluginIdByPlatform(platform) {
|
|
48
|
+
return PLATFORM_TO_PLUGIN_ID[normalizePlatform(platform)] || PLATFORM_TO_PLUGIN_ID.lark;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getDefaultConfig(platform) {
|
|
52
|
+
return { ...(DEFAULT_CONFIGS[normalizePlatform(platform)] || DEFAULT_CONFIGS.lark) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseConfig(value, platform) {
|
|
56
|
+
const defaults = getDefaultConfig(platform);
|
|
57
|
+
if (!value) {
|
|
58
|
+
return defaults;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(value);
|
|
63
|
+
return {
|
|
64
|
+
...defaults,
|
|
65
|
+
...(parsed || {}),
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return defaults;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class ChannelStore {
|
|
73
|
+
normalizePlatform(platform) {
|
|
74
|
+
return normalizePlatform(platform);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getPluginId(platform) {
|
|
78
|
+
return resolvePluginIdByPlatform(platform);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getPluginConfig(pluginId = 'lark_default') {
|
|
82
|
+
const row = db.prepare('SELECT * FROM channel_plugins WHERE id = ?').get(pluginId);
|
|
83
|
+
const platform = resolvePlatformByPluginId(pluginId);
|
|
84
|
+
|
|
85
|
+
if (!row) {
|
|
86
|
+
return {
|
|
87
|
+
id: pluginId,
|
|
88
|
+
type: platform,
|
|
89
|
+
enabled: 0,
|
|
90
|
+
status: 'stopped',
|
|
91
|
+
config: getDefaultConfig(platform),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
id: row.id,
|
|
97
|
+
type: row.type || platform,
|
|
98
|
+
enabled: row.enabled,
|
|
99
|
+
status: row.status,
|
|
100
|
+
config: parseConfig(row.config_json, row.type || platform),
|
|
101
|
+
updatedAt: row.updated_at,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
upsertPluginConfig(pluginId = 'lark_default', patch = {}) {
|
|
106
|
+
const existing = this.getPluginConfig(pluginId);
|
|
107
|
+
const type = resolvePlatformByPluginId(pluginId);
|
|
108
|
+
const nextConfig = {
|
|
109
|
+
...existing.config,
|
|
110
|
+
...patch,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const updatedAt = now();
|
|
114
|
+
|
|
115
|
+
db.prepare(`
|
|
116
|
+
INSERT INTO channel_plugins (id, type, enabled, status, config_json, updated_at)
|
|
117
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
118
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
119
|
+
type = excluded.type,
|
|
120
|
+
enabled = excluded.enabled,
|
|
121
|
+
status = excluded.status,
|
|
122
|
+
config_json = excluded.config_json,
|
|
123
|
+
updated_at = excluded.updated_at
|
|
124
|
+
`).run(
|
|
125
|
+
pluginId,
|
|
126
|
+
type,
|
|
127
|
+
existing.enabled || 0,
|
|
128
|
+
existing.status || 'stopped',
|
|
129
|
+
JSON.stringify(nextConfig),
|
|
130
|
+
updatedAt
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return this.getPluginConfig(pluginId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
updatePluginRuntimeStatus(pluginId = 'lark_default', enabled, status) {
|
|
137
|
+
const existing = this.getPluginConfig(pluginId);
|
|
138
|
+
const type = resolvePlatformByPluginId(pluginId);
|
|
139
|
+
const updatedAt = now();
|
|
140
|
+
|
|
141
|
+
db.prepare(`
|
|
142
|
+
INSERT INTO channel_plugins (id, type, enabled, status, config_json, updated_at)
|
|
143
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
144
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
145
|
+
enabled = excluded.enabled,
|
|
146
|
+
status = excluded.status,
|
|
147
|
+
updated_at = excluded.updated_at
|
|
148
|
+
`).run(
|
|
149
|
+
pluginId,
|
|
150
|
+
type,
|
|
151
|
+
enabled ? 1 : 0,
|
|
152
|
+
status,
|
|
153
|
+
JSON.stringify(existing.config),
|
|
154
|
+
updatedAt
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return this.getPluginConfig(pluginId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
listAllowedUsers(platform = 'lark') {
|
|
161
|
+
const normalizedPlatform = normalizePlatform(platform);
|
|
162
|
+
return db.prepare(`
|
|
163
|
+
SELECT id, platform_type AS platformType, user_id AS userId, display_name AS displayName, note, is_active AS isActive, created_at AS createdAt, updated_at AS updatedAt
|
|
164
|
+
FROM channel_allowed_users
|
|
165
|
+
WHERE platform_type = ?
|
|
166
|
+
ORDER BY created_at DESC
|
|
167
|
+
`).all(normalizedPlatform);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
addAllowedUser(platform = 'lark', { userId, displayName = null, note = null }) {
|
|
171
|
+
const normalizedPlatform = normalizePlatform(platform);
|
|
172
|
+
const trimmedUserId = String(userId || '').trim();
|
|
173
|
+
if (!trimmedUserId) {
|
|
174
|
+
throw new Error('userId is required');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const current = now();
|
|
178
|
+
|
|
179
|
+
db.prepare(`
|
|
180
|
+
INSERT INTO channel_allowed_users (platform_type, user_id, display_name, note, is_active, created_at, updated_at)
|
|
181
|
+
VALUES (?, ?, ?, ?, 1, ?, ?)
|
|
182
|
+
`).run(normalizedPlatform, trimmedUserId, displayName ? String(displayName).trim() : null, note ? String(note).trim() : null, current, current);
|
|
183
|
+
|
|
184
|
+
return db.prepare(`
|
|
185
|
+
SELECT id, platform_type AS platformType, user_id AS userId, display_name AS displayName, note, is_active AS isActive, created_at AS createdAt, updated_at AS updatedAt
|
|
186
|
+
FROM channel_allowed_users
|
|
187
|
+
WHERE platform_type = ? AND user_id = ?
|
|
188
|
+
`).get(normalizedPlatform, trimmedUserId);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
removeAllowedUser(platform = 'lark', id) {
|
|
192
|
+
const normalizedPlatform = normalizePlatform(platform);
|
|
193
|
+
const result = db.prepare('DELETE FROM channel_allowed_users WHERE platform_type = ? AND id = ?').run(normalizedPlatform, id);
|
|
194
|
+
return result.changes > 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
toggleAllowedUser(platform = 'lark', id, isActive) {
|
|
198
|
+
const normalizedPlatform = normalizePlatform(platform);
|
|
199
|
+
const result = db.prepare('UPDATE channel_allowed_users SET is_active = ?, updated_at = ? WHERE platform_type = ? AND id = ?').run(isActive ? 1 : 0, now(), normalizedPlatform, id);
|
|
200
|
+
return result.changes > 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
isAllowedUser(platform = 'lark', userId) {
|
|
204
|
+
const normalizedPlatform = normalizePlatform(platform);
|
|
205
|
+
if (!userId) return false;
|
|
206
|
+
const row = db.prepare('SELECT id FROM channel_allowed_users WHERE platform_type = ? AND user_id = ? AND is_active = 1').get(normalizedPlatform, String(userId));
|
|
207
|
+
return !!row;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getChannelSession({ platform = 'lark', userId, chatId, backend }) {
|
|
211
|
+
const normalizedPlatform = normalizePlatform(platform);
|
|
212
|
+
return db.prepare(`
|
|
213
|
+
SELECT id, platform_type AS platformType, user_id AS userId, chat_id AS chatId, backend, provider_session_id AS providerSessionId, project_path AS projectPath, updated_at AS updatedAt
|
|
214
|
+
FROM channel_sessions
|
|
215
|
+
WHERE platform_type = ? AND user_id = ? AND chat_id = ? AND backend = ?
|
|
216
|
+
LIMIT 1
|
|
217
|
+
`).get(normalizedPlatform, userId, chatId, backend) || null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
upsertChannelSession({ platform = 'lark', userId, chatId, backend, providerSessionId, projectPath }) {
|
|
221
|
+
const normalizedPlatform = normalizePlatform(platform);
|
|
222
|
+
const id = crypto.randomUUID();
|
|
223
|
+
const updatedAt = now();
|
|
224
|
+
|
|
225
|
+
db.prepare(`
|
|
226
|
+
INSERT INTO channel_sessions (id, platform_type, user_id, chat_id, backend, provider_session_id, project_path, updated_at)
|
|
227
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
228
|
+
ON CONFLICT(platform_type, user_id, chat_id, backend) DO UPDATE SET
|
|
229
|
+
provider_session_id = excluded.provider_session_id,
|
|
230
|
+
project_path = excluded.project_path,
|
|
231
|
+
updated_at = excluded.updated_at
|
|
232
|
+
`).run(id, normalizedPlatform, userId, chatId, backend, providerSessionId || null, projectPath || null, updatedAt);
|
|
233
|
+
|
|
234
|
+
return this.getChannelSession({ platform: normalizedPlatform, userId, chatId, backend });
|
|
235
|
+
}
|
|
236
|
+
}
|
package/server/database/db.js
CHANGED
|
@@ -55,6 +55,111 @@ if (process.env.DATABASE_PATH) {
|
|
|
55
55
|
console.log(c.dim('═'.repeat(60)));
|
|
56
56
|
console.log('');
|
|
57
57
|
|
|
58
|
+
const tableExists = (tableName) => {
|
|
59
|
+
const row = db
|
|
60
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
61
|
+
.get(tableName);
|
|
62
|
+
return !!row;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const getTableColumns = (tableName) => {
|
|
66
|
+
if (!tableExists(tableName)) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
return db.prepare(`PRAGMA table_info(${tableName})`).all().map((col) => col.name);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const hasIndex = (indexName) => {
|
|
73
|
+
const row = db
|
|
74
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?")
|
|
75
|
+
.get(indexName);
|
|
76
|
+
return !!row;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const ensureChannelAllowedUsersSchema = () => {
|
|
80
|
+
if (!tableExists('channel_allowed_users')) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const columns = getTableColumns('channel_allowed_users');
|
|
85
|
+
const tableSqlRow = db
|
|
86
|
+
.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'channel_allowed_users'")
|
|
87
|
+
.get();
|
|
88
|
+
const tableSql = String(tableSqlRow?.sql || '');
|
|
89
|
+
const hasLegacyUserIdUnique = /user_id\s+TEXT\s+UNIQUE/i.test(tableSql);
|
|
90
|
+
const needsRebuild = !columns.includes('platform_type') || hasLegacyUserIdUnique;
|
|
91
|
+
|
|
92
|
+
if (needsRebuild) {
|
|
93
|
+
console.log('Running migration: Rebuilding channel_allowed_users with platform isolation');
|
|
94
|
+
const beforeCount = db.prepare('SELECT COUNT(*) AS count FROM channel_allowed_users').get()?.count || 0;
|
|
95
|
+
|
|
96
|
+
db.transaction(() => {
|
|
97
|
+
db.exec('ALTER TABLE channel_allowed_users RENAME TO channel_allowed_users_old');
|
|
98
|
+
db.exec(`
|
|
99
|
+
CREATE TABLE channel_allowed_users (
|
|
100
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
101
|
+
platform_type TEXT NOT NULL DEFAULT 'lark',
|
|
102
|
+
user_id TEXT NOT NULL,
|
|
103
|
+
display_name TEXT,
|
|
104
|
+
note TEXT,
|
|
105
|
+
is_active BOOLEAN DEFAULT 1,
|
|
106
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
107
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
108
|
+
)
|
|
109
|
+
`);
|
|
110
|
+
|
|
111
|
+
const oldColumns = getTableColumns('channel_allowed_users_old');
|
|
112
|
+
const hasOldPlatformType = oldColumns.includes('platform_type');
|
|
113
|
+
|
|
114
|
+
if (hasOldPlatformType) {
|
|
115
|
+
db.exec(`
|
|
116
|
+
INSERT INTO channel_allowed_users (id, platform_type, user_id, display_name, note, is_active, created_at, updated_at)
|
|
117
|
+
SELECT id, COALESCE(platform_type, 'lark'), user_id, display_name, note, is_active, created_at, updated_at
|
|
118
|
+
FROM channel_allowed_users_old
|
|
119
|
+
`);
|
|
120
|
+
} else {
|
|
121
|
+
db.exec(`
|
|
122
|
+
INSERT INTO channel_allowed_users (id, platform_type, user_id, display_name, note, is_active, created_at, updated_at)
|
|
123
|
+
SELECT id, 'lark', user_id, display_name, note, is_active, created_at, updated_at
|
|
124
|
+
FROM channel_allowed_users_old
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
db.exec('DROP TABLE channel_allowed_users_old');
|
|
129
|
+
})();
|
|
130
|
+
|
|
131
|
+
const afterCount = db.prepare('SELECT COUNT(*) AS count FROM channel_allowed_users').get()?.count || 0;
|
|
132
|
+
console.log(`channel_allowed_users rows migrated: ${beforeCount} -> ${afterCount}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
db.exec("UPDATE channel_allowed_users SET platform_type = 'lark' WHERE platform_type IS NULL OR TRIM(platform_type) = ''");
|
|
136
|
+
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_allowed_users_platform_user ON channel_allowed_users(platform_type, user_id)');
|
|
137
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_user_id ON channel_allowed_users(user_id)');
|
|
138
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_platform_type ON channel_allowed_users(platform_type)');
|
|
139
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_active ON channel_allowed_users(is_active)');
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const ensureChannelSessionsSchema = () => {
|
|
143
|
+
if (!tableExists('channel_sessions')) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const columns = getTableColumns('channel_sessions');
|
|
148
|
+
if (!columns.includes('platform_type')) {
|
|
149
|
+
console.log('Running migration: Adding platform_type to channel_sessions');
|
|
150
|
+
db.exec("ALTER TABLE channel_sessions ADD COLUMN platform_type TEXT NOT NULL DEFAULT 'lark'");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
db.exec("UPDATE channel_sessions SET platform_type = 'lark' WHERE platform_type IS NULL OR TRIM(platform_type) = ''");
|
|
154
|
+
|
|
155
|
+
if (hasIndex('idx_channel_sessions_unique')) {
|
|
156
|
+
db.exec('DROP INDEX IF EXISTS idx_channel_sessions_unique');
|
|
157
|
+
}
|
|
158
|
+
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_sessions_unique ON channel_sessions(platform_type, user_id, chat_id, backend)');
|
|
159
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_channel_sessions_platform_type ON channel_sessions(platform_type)');
|
|
160
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_channel_sessions_user_id ON channel_sessions(user_id)');
|
|
161
|
+
};
|
|
162
|
+
|
|
58
163
|
const runMigrations = () => {
|
|
59
164
|
try {
|
|
60
165
|
const tableInfo = db.prepare("PRAGMA table_info(users)").all();
|
|
@@ -75,6 +180,9 @@ const runMigrations = () => {
|
|
|
75
180
|
db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0');
|
|
76
181
|
}
|
|
77
182
|
|
|
183
|
+
ensureChannelAllowedUsersSchema();
|
|
184
|
+
ensureChannelSessionsSchema();
|
|
185
|
+
|
|
78
186
|
console.log('Database migrations completed successfully');
|
|
79
187
|
} catch (error) {
|
|
80
188
|
console.error('Error running migrations:', error.message);
|
|
@@ -358,4 +466,4 @@ export {
|
|
|
358
466
|
apiKeysDb,
|
|
359
467
|
credentialsDb,
|
|
360
468
|
githubTokensDb // Backward compatibility
|
|
361
|
-
};
|
|
469
|
+
};
|
package/server/database/init.sql
CHANGED
|
@@ -49,4 +49,50 @@ CREATE TABLE IF NOT EXISTS user_credentials (
|
|
|
49
49
|
|
|
50
50
|
CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id);
|
|
51
51
|
CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
|
|
52
|
-
CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
|
|
53
|
+
|
|
54
|
+
-- Channel plugins table (Lark and future channels)
|
|
55
|
+
CREATE TABLE IF NOT EXISTS channel_plugins (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
type TEXT NOT NULL,
|
|
58
|
+
enabled BOOLEAN DEFAULT 0,
|
|
59
|
+
status TEXT DEFAULT 'stopped',
|
|
60
|
+
config_json TEXT NOT NULL,
|
|
61
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_channel_plugins_type ON channel_plugins(type);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_channel_plugins_enabled ON channel_plugins(enabled);
|
|
66
|
+
|
|
67
|
+
-- Allowed channel users (whitelist by platform user ID)
|
|
68
|
+
CREATE TABLE IF NOT EXISTS channel_allowed_users (
|
|
69
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
70
|
+
platform_type TEXT NOT NULL DEFAULT 'lark',
|
|
71
|
+
user_id TEXT NOT NULL,
|
|
72
|
+
display_name TEXT,
|
|
73
|
+
note TEXT,
|
|
74
|
+
is_active BOOLEAN DEFAULT 1,
|
|
75
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
76
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_allowed_users_platform_user ON channel_allowed_users(platform_type, user_id);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_user_id ON channel_allowed_users(user_id);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_platform_type ON channel_allowed_users(platform_type);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_active ON channel_allowed_users(is_active);
|
|
83
|
+
|
|
84
|
+
-- Channel session bindings
|
|
85
|
+
CREATE TABLE IF NOT EXISTS channel_sessions (
|
|
86
|
+
id TEXT PRIMARY KEY,
|
|
87
|
+
platform_type TEXT NOT NULL DEFAULT 'lark',
|
|
88
|
+
user_id TEXT NOT NULL,
|
|
89
|
+
chat_id TEXT NOT NULL,
|
|
90
|
+
backend TEXT NOT NULL,
|
|
91
|
+
provider_session_id TEXT,
|
|
92
|
+
project_path TEXT,
|
|
93
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_sessions_unique ON channel_sessions(platform_type, user_id, chat_id, backend);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_channel_sessions_platform_type ON channel_sessions(platform_type);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_channel_sessions_user_id ON channel_sessions(user_id);
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import crossSpawn from 'cross-spawn';
|
|
3
|
+
|
|
4
|
+
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
5
|
+
|
|
6
|
+
const activeGeminiProcesses = new Map();
|
|
7
|
+
|
|
8
|
+
function collectTextChunks(payload) {
|
|
9
|
+
if (!payload) return [];
|
|
10
|
+
if (typeof payload === 'string') return payload.trim() ? [payload] : [];
|
|
11
|
+
|
|
12
|
+
const chunks = [];
|
|
13
|
+
|
|
14
|
+
if (Array.isArray(payload)) {
|
|
15
|
+
payload.forEach(item => {
|
|
16
|
+
chunks.push(...collectTextChunks(item));
|
|
17
|
+
});
|
|
18
|
+
return chunks;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof payload !== 'object') {
|
|
22
|
+
return chunks;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const directKeys = ['text', 'response', 'content', 'message', 'delta'];
|
|
26
|
+
for (const key of directKeys) {
|
|
27
|
+
if (payload[key] !== undefined) {
|
|
28
|
+
chunks.push(...collectTextChunks(payload[key]));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(payload.parts)) {
|
|
33
|
+
payload.parts.forEach(part => {
|
|
34
|
+
chunks.push(...collectTextChunks(part));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (payload.data && typeof payload.data === 'object') {
|
|
39
|
+
chunks.push(...collectTextChunks(payload.data));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return chunks;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getEventRole(event) {
|
|
46
|
+
return (
|
|
47
|
+
event?.role ||
|
|
48
|
+
event?.author ||
|
|
49
|
+
event?.sender ||
|
|
50
|
+
event?.message?.role ||
|
|
51
|
+
event?.data?.role ||
|
|
52
|
+
event?.content?.role ||
|
|
53
|
+
null
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractAssistantTextChunks(event, command) {
|
|
58
|
+
const role = String(getEventRole(event) || '').toLowerCase();
|
|
59
|
+
if (role === 'user') return [];
|
|
60
|
+
|
|
61
|
+
const eventType = String(event?.type || event?.event || event?.kind || '').toLowerCase();
|
|
62
|
+
const hasAssistantPayload = !!(event?.response || event?.candidates || event?.delta || event?.text || event?.content || event?.message);
|
|
63
|
+
if (eventType.includes('prompt') && !hasAssistantPayload) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const normalizedPrompt = String(command || '').trim();
|
|
68
|
+
return collectTextChunks(event).filter((text) => {
|
|
69
|
+
const trimmed = String(text || '').trim();
|
|
70
|
+
if (!trimmed) return false;
|
|
71
|
+
if (normalizedPrompt && trimmed === normalizedPrompt) return false;
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseGeminiJsonLine(line) {
|
|
77
|
+
const trimmed = line.trim();
|
|
78
|
+
if (!trimmed) return null;
|
|
79
|
+
|
|
80
|
+
const payload = trimmed.startsWith('data:') ? trimmed.slice(5).trim() : trimmed;
|
|
81
|
+
if (!payload) return null;
|
|
82
|
+
|
|
83
|
+
return JSON.parse(payload);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function emitTextChunks(ws, sessionId, chunks) {
|
|
87
|
+
chunks.forEach((text) => {
|
|
88
|
+
if (!text) return;
|
|
89
|
+
ws.send({
|
|
90
|
+
type: 'claude-response',
|
|
91
|
+
data: {
|
|
92
|
+
type: 'content_block_delta',
|
|
93
|
+
delta: {
|
|
94
|
+
type: 'text_delta',
|
|
95
|
+
text
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
provider: 'gemini',
|
|
99
|
+
sessionId
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function queryGemini(command, options = {}, ws) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const { sessionId, cwd, projectPath, resume, model, permissionMode } = options;
|
|
107
|
+
let capturedSessionId = sessionId;
|
|
108
|
+
let sentSessionCreated = false;
|
|
109
|
+
|
|
110
|
+
const args = ['-y', '@google/gemini-cli'];
|
|
111
|
+
|
|
112
|
+
if (sessionId && (resume || !command || !command.trim())) {
|
|
113
|
+
args.push('--resume', sessionId);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (command && command.trim()) {
|
|
117
|
+
args.push('--prompt', command);
|
|
118
|
+
args.push('--output-format', 'stream-json');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (model) {
|
|
122
|
+
args.push('--model', model);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (permissionMode === 'bypassPermissions' || permissionMode === 'acceptEdits') {
|
|
126
|
+
args.push('--yolo');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const workingDir = cwd || projectPath || process.cwd();
|
|
130
|
+
|
|
131
|
+
const geminiProcess = spawnFunction('npx', args, {
|
|
132
|
+
cwd: workingDir,
|
|
133
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
134
|
+
env: {
|
|
135
|
+
...process.env,
|
|
136
|
+
GEMINI_NONINTERACTIVE: '1'
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const processKey = capturedSessionId || Date.now().toString();
|
|
141
|
+
let processRegistryKey = processKey;
|
|
142
|
+
activeGeminiProcesses.set(processRegistryKey, geminiProcess);
|
|
143
|
+
|
|
144
|
+
const finalizeSessionId = () => capturedSessionId || sessionId || null;
|
|
145
|
+
|
|
146
|
+
const handleJsonEvent = (event) => {
|
|
147
|
+
const incomingSessionId = event?.session_id || event?.sessionId || event?.data?.session_id || event?.data?.sessionId;
|
|
148
|
+
if (incomingSessionId && !capturedSessionId) {
|
|
149
|
+
capturedSessionId = incomingSessionId;
|
|
150
|
+
if (ws.setSessionId && typeof ws.setSessionId === 'function') {
|
|
151
|
+
ws.setSessionId(capturedSessionId);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (processRegistryKey !== capturedSessionId) {
|
|
155
|
+
activeGeminiProcesses.delete(processRegistryKey);
|
|
156
|
+
activeGeminiProcesses.set(capturedSessionId, geminiProcess);
|
|
157
|
+
processRegistryKey = capturedSessionId;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!sessionId && !sentSessionCreated) {
|
|
161
|
+
sentSessionCreated = true;
|
|
162
|
+
ws.send({
|
|
163
|
+
type: 'session-created',
|
|
164
|
+
sessionId: capturedSessionId,
|
|
165
|
+
provider: 'gemini'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const type = event?.type || event?.event || event?.kind;
|
|
171
|
+
if (type === 'result' || type === 'done' || type === 'complete') {
|
|
172
|
+
ws.send({
|
|
173
|
+
type: 'claude-response',
|
|
174
|
+
data: { type: 'content_block_stop' },
|
|
175
|
+
provider: 'gemini',
|
|
176
|
+
sessionId: finalizeSessionId()
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
ws.send({
|
|
180
|
+
type: 'gemini-result',
|
|
181
|
+
data: event,
|
|
182
|
+
sessionId: finalizeSessionId()
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const textChunks = extractAssistantTextChunks(event, command);
|
|
188
|
+
emitTextChunks(ws, finalizeSessionId(), textChunks);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
let stdoutBuffer = '';
|
|
192
|
+
geminiProcess.stdout.on('data', (data) => {
|
|
193
|
+
stdoutBuffer += data.toString();
|
|
194
|
+
const lines = stdoutBuffer.split('\n');
|
|
195
|
+
stdoutBuffer = lines.pop() || '';
|
|
196
|
+
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
try {
|
|
199
|
+
const event = parseGeminiJsonLine(line);
|
|
200
|
+
if (!event) continue;
|
|
201
|
+
handleJsonEvent(event);
|
|
202
|
+
} catch {}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
geminiProcess.stderr.on('data', (data) => {
|
|
207
|
+
ws.send({
|
|
208
|
+
type: 'claude-error',
|
|
209
|
+
error: data.toString(),
|
|
210
|
+
sessionId: finalizeSessionId()
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
geminiProcess.on('close', (code) => {
|
|
215
|
+
if (stdoutBuffer.trim()) {
|
|
216
|
+
try {
|
|
217
|
+
const finalEvent = parseGeminiJsonLine(stdoutBuffer);
|
|
218
|
+
if (finalEvent) {
|
|
219
|
+
handleJsonEvent(finalEvent);
|
|
220
|
+
}
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
activeGeminiProcesses.delete(processRegistryKey);
|
|
225
|
+
|
|
226
|
+
ws.send({
|
|
227
|
+
type: 'claude-complete',
|
|
228
|
+
sessionId: finalizeSessionId(),
|
|
229
|
+
provider: 'gemini',
|
|
230
|
+
exitCode: code,
|
|
231
|
+
isNewSession: !sessionId && !!command
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (code === 0) {
|
|
235
|
+
resolve();
|
|
236
|
+
} else {
|
|
237
|
+
reject(new Error(`Gemini CLI exited with code ${code}`));
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
geminiProcess.on('error', (error) => {
|
|
242
|
+
activeGeminiProcesses.delete(processRegistryKey);
|
|
243
|
+
|
|
244
|
+
ws.send({
|
|
245
|
+
type: 'claude-error',
|
|
246
|
+
error: error.message,
|
|
247
|
+
sessionId: finalizeSessionId()
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
reject(error);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
geminiProcess.stdin.end();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function abortGeminiSession(sessionId) {
|
|
258
|
+
const process = activeGeminiProcesses.get(sessionId);
|
|
259
|
+
if (process) {
|
|
260
|
+
process.kill('SIGTERM');
|
|
261
|
+
activeGeminiProcesses.delete(sessionId);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isGeminiSessionActive(sessionId) {
|
|
268
|
+
return activeGeminiProcesses.has(sessionId);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getActiveGeminiSessions() {
|
|
272
|
+
return Array.from(activeGeminiProcesses.keys());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export {
|
|
276
|
+
queryGemini,
|
|
277
|
+
abortGeminiSession,
|
|
278
|
+
isGeminiSessionActive,
|
|
279
|
+
getActiveGeminiSessions
|
|
280
|
+
};
|