@mmmbuto/nexuscli 0.9.5 → 0.9.7-termux
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 +28 -5
- package/lib/cli/engines.js +51 -2
- package/lib/config/manager.js +5 -0
- package/lib/config/models.js +28 -0
- package/lib/server/lib/cli-wrapper.js +167 -0
- package/lib/server/lib/output-parser.js +132 -0
- package/lib/server/lib/pty-adapter.js +81 -0
- package/lib/server/middleware/rate-limit.js +1 -1
- package/lib/server/models/Message.js +1 -1
- package/lib/server/routes/models.js +2 -0
- package/lib/server/routes/qwen.js +240 -0
- package/lib/server/routes/sessions.js +13 -1
- package/lib/server/server.js +7 -4
- package/lib/server/services/cli-loader.js +83 -3
- package/lib/server/services/context-bridge.js +4 -2
- package/lib/server/services/qwen-output-parser.js +289 -0
- package/lib/server/services/qwen-wrapper.js +251 -0
- package/lib/server/services/session-importer.js +35 -2
- package/lib/server/services/session-manager.js +32 -5
- package/lib/server/tests/history-sync.test.js +11 -2
- package/lib/server/tests/integration-session-sync.test.js +40 -8
- package/lib/server/tests/integration.test.js +33 -16
- package/lib/server/tests/performance.test.js +16 -9
- package/lib/server/tests/services.test.js +17 -10
- package/package.json +2 -2
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qwen Route - /api/v1/qwen
|
|
3
|
+
*
|
|
4
|
+
* Send messages to Qwen Code CLI with SSE streaming.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const express = require('express');
|
|
8
|
+
const QwenWrapper = require('../services/qwen-wrapper');
|
|
9
|
+
const Message = require('../models/Message');
|
|
10
|
+
const { v4: uuidv4 } = require('uuid');
|
|
11
|
+
const sessionManager = require('../services/session-manager');
|
|
12
|
+
const contextBridge = require('../services/context-bridge');
|
|
13
|
+
const { resolveWorkspacePath } = require('../../utils/workspace');
|
|
14
|
+
|
|
15
|
+
const router = express.Router();
|
|
16
|
+
const qwenWrapper = new QwenWrapper();
|
|
17
|
+
|
|
18
|
+
function ensureConversation(conversationId, workspacePath) {
|
|
19
|
+
try {
|
|
20
|
+
const stmt = require('../db').prepare(`
|
|
21
|
+
INSERT OR IGNORE INTO conversations (id, title, created_at, updated_at, metadata)
|
|
22
|
+
VALUES (?, ?, ?, ?, ?)
|
|
23
|
+
`);
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const metadata = workspacePath ? JSON.stringify({ workspace: workspacePath }) : null;
|
|
26
|
+
stmt.run(conversationId, 'New Chat', now, now, metadata);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.warn('[Qwen] Failed to ensure conversation exists:', err.message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* POST /api/v1/qwen
|
|
34
|
+
* Body:
|
|
35
|
+
* {
|
|
36
|
+
* conversationId?: string,
|
|
37
|
+
* message: string,
|
|
38
|
+
* model?: string,
|
|
39
|
+
* workspace?: string
|
|
40
|
+
* }
|
|
41
|
+
*/
|
|
42
|
+
router.post('/', async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
console.log('[Qwen] === NEW QWEN REQUEST ===');
|
|
45
|
+
console.log('[Qwen] Body:', JSON.stringify(req.body, null, 2));
|
|
46
|
+
|
|
47
|
+
const { conversationId, message, model = 'coder-model', workspace } = req.body;
|
|
48
|
+
|
|
49
|
+
if (!message) {
|
|
50
|
+
return res.status(400).json({ error: 'message required' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isAvailable = await qwenWrapper.isAvailable();
|
|
54
|
+
if (!isAvailable) {
|
|
55
|
+
return res.status(503).json({
|
|
56
|
+
error: 'Qwen CLI not available',
|
|
57
|
+
details: 'Please install Qwen CLI: npm install -g @mmmbuto/qwen-code-termux'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const workspacePath = resolveWorkspacePath(workspace, process.cwd());
|
|
62
|
+
if (workspace && workspacePath !== workspace) {
|
|
63
|
+
console.warn(`[Qwen] Workspace corrected: ${workspace} → ${workspacePath}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const frontendConversationId = conversationId || uuidv4();
|
|
67
|
+
ensureConversation(frontendConversationId, workspacePath);
|
|
68
|
+
|
|
69
|
+
const { sessionId, isNew: isNewSession } = await sessionManager.getOrCreateSession(
|
|
70
|
+
frontendConversationId,
|
|
71
|
+
'qwen',
|
|
72
|
+
workspacePath
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const nativeSessionId = isNewSession ? null : sessionManager.getNativeThreadId(sessionId);
|
|
76
|
+
|
|
77
|
+
// SSE headers
|
|
78
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
79
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
80
|
+
res.setHeader('Connection', 'keep-alive');
|
|
81
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
82
|
+
|
|
83
|
+
res.write(`data: ${JSON.stringify({
|
|
84
|
+
type: 'message_start',
|
|
85
|
+
messageId: `user-${Date.now()}`,
|
|
86
|
+
sessionId,
|
|
87
|
+
conversationId: frontendConversationId,
|
|
88
|
+
engine: 'qwen'
|
|
89
|
+
})}\n\n`);
|
|
90
|
+
|
|
91
|
+
const lastEngine = Message.getLastEngine(frontendConversationId);
|
|
92
|
+
const isEngineBridge = lastEngine && lastEngine !== 'qwen';
|
|
93
|
+
|
|
94
|
+
let promptToSend = message;
|
|
95
|
+
|
|
96
|
+
if (isEngineBridge) {
|
|
97
|
+
const contextResult = await contextBridge.buildContext({
|
|
98
|
+
conversationId: frontendConversationId,
|
|
99
|
+
sessionId,
|
|
100
|
+
fromEngine: lastEngine,
|
|
101
|
+
toEngine: 'qwen',
|
|
102
|
+
userMessage: message
|
|
103
|
+
});
|
|
104
|
+
promptToSend = contextResult.prompt;
|
|
105
|
+
|
|
106
|
+
res.write(`data: ${JSON.stringify({
|
|
107
|
+
type: 'status',
|
|
108
|
+
category: 'system',
|
|
109
|
+
message: `Context bridged from ${lastEngine}`,
|
|
110
|
+
icon: '🔄'
|
|
111
|
+
})}\n\n`);
|
|
112
|
+
} else if (nativeSessionId) {
|
|
113
|
+
console.log(`[Qwen] Native resume: qwen --resume ${nativeSessionId}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Save user message
|
|
117
|
+
try {
|
|
118
|
+
Message.create(
|
|
119
|
+
frontendConversationId,
|
|
120
|
+
'user',
|
|
121
|
+
message,
|
|
122
|
+
{ workspace: workspacePath, model },
|
|
123
|
+
Date.now(),
|
|
124
|
+
'qwen'
|
|
125
|
+
);
|
|
126
|
+
sessionManager.bumpSessionActivity(sessionId, 1);
|
|
127
|
+
} catch (dbErr) {
|
|
128
|
+
console.warn('[Qwen] Failed to save user message:', dbErr.message);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isNewSession) {
|
|
132
|
+
const title = sessionManager.extractTitle(message);
|
|
133
|
+
sessionManager.updateSessionTitle(sessionId, title);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = await qwenWrapper.sendMessage({
|
|
137
|
+
prompt: promptToSend,
|
|
138
|
+
threadId: nativeSessionId,
|
|
139
|
+
model,
|
|
140
|
+
workspacePath,
|
|
141
|
+
processId: sessionId,
|
|
142
|
+
onStatus: (event) => {
|
|
143
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (result.sessionId) {
|
|
148
|
+
sessionManager.setNativeThreadId(sessionId, result.sessionId);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
Message.create(
|
|
153
|
+
frontendConversationId,
|
|
154
|
+
'assistant',
|
|
155
|
+
result.text,
|
|
156
|
+
{ model, usage: result.usage },
|
|
157
|
+
Date.now(),
|
|
158
|
+
'qwen'
|
|
159
|
+
);
|
|
160
|
+
sessionManager.bumpSessionActivity(sessionId, 1);
|
|
161
|
+
} catch (dbErr) {
|
|
162
|
+
console.warn('[Qwen] Failed to save assistant message:', dbErr.message);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (contextBridge.shouldTriggerSummary(frontendConversationId, isEngineBridge)) {
|
|
166
|
+
contextBridge.triggerSummaryGeneration(frontendConversationId, '[Qwen]');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
res.write(`data: ${JSON.stringify({
|
|
170
|
+
type: 'message_done',
|
|
171
|
+
content: result.text,
|
|
172
|
+
usage: result.usage,
|
|
173
|
+
sessionId,
|
|
174
|
+
conversationId: frontendConversationId,
|
|
175
|
+
engine: 'qwen',
|
|
176
|
+
model
|
|
177
|
+
})}\n\n`);
|
|
178
|
+
|
|
179
|
+
res.end();
|
|
180
|
+
console.log('[Qwen] === REQUEST COMPLETE ===');
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('[Qwen] Error:', error);
|
|
183
|
+
if (!res.headersSent) {
|
|
184
|
+
res.status(500).json({ error: error.message });
|
|
185
|
+
} else {
|
|
186
|
+
res.write(`data: ${JSON.stringify({
|
|
187
|
+
type: 'error',
|
|
188
|
+
error: error.message
|
|
189
|
+
})}\n\n`);
|
|
190
|
+
res.end();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* GET /api/v1/qwen/status
|
|
197
|
+
*/
|
|
198
|
+
router.get('/status', async (_req, res) => {
|
|
199
|
+
try {
|
|
200
|
+
const isAvailable = await qwenWrapper.isAvailable();
|
|
201
|
+
res.json({
|
|
202
|
+
available: isAvailable,
|
|
203
|
+
defaultModel: qwenWrapper.getDefaultModel(),
|
|
204
|
+
models: qwenWrapper.getAvailableModels()
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('[Qwen] Status check error:', error);
|
|
208
|
+
res.status(500).json({ error: error.message });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* GET /api/v1/qwen/models
|
|
214
|
+
*/
|
|
215
|
+
router.get('/models', (_req, res) => {
|
|
216
|
+
res.json({
|
|
217
|
+
models: qwenWrapper.getAvailableModels(),
|
|
218
|
+
default: qwenWrapper.getDefaultModel()
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* POST /api/v1/qwen/interrupt
|
|
224
|
+
*/
|
|
225
|
+
router.post('/interrupt', async (req, res) => {
|
|
226
|
+
try {
|
|
227
|
+
const { sessionId } = req.body;
|
|
228
|
+
if (!sessionId) {
|
|
229
|
+
return res.status(400).json({ error: 'sessionId required' });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const result = qwenWrapper.interrupt(sessionId);
|
|
233
|
+
res.json(result);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('[Qwen] Interrupt error:', error);
|
|
236
|
+
res.status(500).json({ error: error.message });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
module.exports = router;
|
|
@@ -15,11 +15,12 @@ const SESSION_DIRS = {
|
|
|
15
15
|
claude: path.join(process.env.HOME || '', '.claude', 'projects'),
|
|
16
16
|
codex: path.join(process.env.HOME || '', '.codex', 'sessions'),
|
|
17
17
|
gemini: path.join(process.env.HOME || '', '.gemini', 'sessions'),
|
|
18
|
+
qwen: path.join(process.env.HOME || '', '.qwen', 'projects'),
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* POST /api/v1/sessions/import
|
|
22
|
-
* Importa tutte le sessioni native (Claude/Codex/Gemini) nel DB
|
|
23
|
+
* Importa tutte le sessioni native (Claude/Codex/Gemini/Qwen) nel DB
|
|
23
24
|
*/
|
|
24
25
|
router.post('/import', async (_req, res) => {
|
|
25
26
|
try {
|
|
@@ -280,6 +281,11 @@ function pathToSlug(workspacePath) {
|
|
|
280
281
|
return workspacePath.replace(/[\/\.]/g, '-');
|
|
281
282
|
}
|
|
282
283
|
|
|
284
|
+
function qwenProjectDir(workspacePath) {
|
|
285
|
+
if (!workspacePath) return 'default';
|
|
286
|
+
return workspacePath.replace(/[^a-zA-Z0-9]/g, '-');
|
|
287
|
+
}
|
|
288
|
+
|
|
283
289
|
/**
|
|
284
290
|
* Helper: Get the filesystem path for a session file
|
|
285
291
|
*/
|
|
@@ -287,6 +293,7 @@ function getSessionFilePath(sessionId, engine, workspacePath, sessionPath) {
|
|
|
287
293
|
const normalizedEngine = engine?.toLowerCase().includes('claude') ? 'claude'
|
|
288
294
|
: engine?.toLowerCase().includes('codex') ? 'codex'
|
|
289
295
|
: engine?.toLowerCase().includes('gemini') ? 'gemini'
|
|
296
|
+
: engine?.toLowerCase().includes('qwen') ? 'qwen'
|
|
290
297
|
: 'claude';
|
|
291
298
|
|
|
292
299
|
switch (normalizedEngine) {
|
|
@@ -303,6 +310,11 @@ function getSessionFilePath(sessionId, engine, workspacePath, sessionPath) {
|
|
|
303
310
|
return findCodexSessionFile(baseDir, nativeId);
|
|
304
311
|
case 'gemini':
|
|
305
312
|
return path.join(SESSION_DIRS.gemini, `${sessionId}.jsonl`);
|
|
313
|
+
case 'qwen': {
|
|
314
|
+
const project = qwenProjectDir(workspacePath);
|
|
315
|
+
const fileId = sessionPath || sessionId;
|
|
316
|
+
return path.join(SESSION_DIRS.qwen, project, 'chats', `${fileId}.jsonl`);
|
|
317
|
+
}
|
|
306
318
|
default:
|
|
307
319
|
return null;
|
|
308
320
|
}
|
package/lib/server/server.js
CHANGED
|
@@ -22,6 +22,7 @@ const jobsRouter = require('./routes/jobs');
|
|
|
22
22
|
const chatRouter = require('./routes/chat');
|
|
23
23
|
const codexRouter = require('./routes/codex');
|
|
24
24
|
const geminiRouter = require('./routes/gemini');
|
|
25
|
+
const qwenRouter = require('./routes/qwen');
|
|
25
26
|
const modelsRouter = require('./routes/models');
|
|
26
27
|
const workspaceRouter = require('./routes/workspace');
|
|
27
28
|
const workspacesRouter = require('./routes/workspaces');
|
|
@@ -51,7 +52,7 @@ app.get('/health', (req, res) => {
|
|
|
51
52
|
status: 'ok',
|
|
52
53
|
service: 'nexuscli-backend',
|
|
53
54
|
version: pkg.version,
|
|
54
|
-
engines: ['claude', 'codex', 'gemini'],
|
|
55
|
+
engines: ['claude', 'codex', 'gemini', 'qwen'],
|
|
55
56
|
port: PORT,
|
|
56
57
|
timestamp: new Date().toISOString()
|
|
57
58
|
});
|
|
@@ -78,6 +79,7 @@ app.use('/api/v1/jobs', authMiddleware, jobsRouter);
|
|
|
78
79
|
app.use('/api/v1/chat', authMiddleware, chatRateLimiter, chatRouter);
|
|
79
80
|
app.use('/api/v1/codex', authMiddleware, chatRateLimiter, codexRouter);
|
|
80
81
|
app.use('/api/v1/gemini', authMiddleware, chatRateLimiter, geminiRouter);
|
|
82
|
+
app.use('/api/v1/qwen', authMiddleware, chatRateLimiter, qwenRouter);
|
|
81
83
|
app.use('/api/v1/upload', authMiddleware, uploadRouter); // File upload
|
|
82
84
|
|
|
83
85
|
// STT routes
|
|
@@ -89,13 +91,14 @@ app.get('/', (req, res) => {
|
|
|
89
91
|
res.json({
|
|
90
92
|
service: 'NexusCLI Backend',
|
|
91
93
|
version: pkg.version,
|
|
92
|
-
engines: ['claude', 'codex', 'gemini'],
|
|
94
|
+
engines: ['claude', 'codex', 'gemini', 'qwen'],
|
|
93
95
|
endpoints: {
|
|
94
96
|
health: '/health',
|
|
95
97
|
models: '/api/v1/models',
|
|
96
98
|
chat: '/api/v1/chat (Claude)',
|
|
97
99
|
codex: '/api/v1/codex (OpenAI)',
|
|
98
100
|
gemini: '/api/v1/gemini (Google)',
|
|
101
|
+
qwen: '/api/v1/qwen (Qwen)',
|
|
99
102
|
conversations: '/api/v1/conversations',
|
|
100
103
|
jobs: '/api/v1/jobs'
|
|
101
104
|
}
|
|
@@ -154,10 +157,10 @@ async function start() {
|
|
|
154
157
|
// Continue anyway - database will sync on first workspace mount
|
|
155
158
|
}
|
|
156
159
|
|
|
157
|
-
// Import native sessions from all engines (Claude/Codex/Gemini)
|
|
160
|
+
// Import native sessions from all engines (Claude/Codex/Gemini/Qwen)
|
|
158
161
|
try {
|
|
159
162
|
const imported = sessionImporter.importAll();
|
|
160
|
-
console.log(`[Startup] Imported sessions → Claude:${imported.claude} Codex:${imported.codex} Gemini:${imported.gemini}`);
|
|
163
|
+
console.log(`[Startup] Imported sessions → Claude:${imported.claude} Codex:${imported.codex} Gemini:${imported.gemini} Qwen:${imported.qwen}`);
|
|
161
164
|
} catch (error) {
|
|
162
165
|
console.error('[Startup] ⚠️ Session import failed:', error.message);
|
|
163
166
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CliLoader - Unified message loader for TRI CLI (Claude/Codex/Gemini)
|
|
2
|
+
* CliLoader - Unified message loader for TRI CLI (Claude/Codex/Gemini/Qwen)
|
|
3
3
|
*
|
|
4
4
|
* Loads messages on-demand from CLI history files (lazy loading).
|
|
5
5
|
* Filesystem is the source of truth - no DB caching of messages.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - Claude: ~/.claude/projects/<workspace-slug>/<sessionId>.jsonl
|
|
9
9
|
* - Codex: ~/.codex/sessions/<sessionId>.jsonl (if available)
|
|
10
10
|
* - Gemini: ~/.gemini/sessions/<sessionId>.jsonl (if available)
|
|
11
|
+
* - Qwen : ~/.qwen/projects/<sanitized>/chats/<sessionId>.jsonl
|
|
11
12
|
*
|
|
12
13
|
* @version 0.4.0 - TRI CLI Support
|
|
13
14
|
*/
|
|
@@ -23,6 +24,7 @@ const ENGINE_PATHS = {
|
|
|
23
24
|
claude: path.join(process.env.HOME || '', '.claude'),
|
|
24
25
|
codex: path.join(process.env.HOME || '', '.codex'),
|
|
25
26
|
gemini: path.join(process.env.HOME || '', '.gemini'),
|
|
27
|
+
qwen: path.join(process.env.HOME || '', '.qwen'),
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
class CliLoader {
|
|
@@ -30,15 +32,16 @@ class CliLoader {
|
|
|
30
32
|
this.claudePath = ENGINE_PATHS.claude;
|
|
31
33
|
this.codexPath = ENGINE_PATHS.codex;
|
|
32
34
|
this.geminiPath = ENGINE_PATHS.gemini;
|
|
35
|
+
this.qwenPath = ENGINE_PATHS.qwen;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
39
|
* Load messages from CLI history by session.
|
|
37
|
-
* Supports all
|
|
40
|
+
* Supports all engines: Claude, Codex, Gemini, Qwen.
|
|
38
41
|
*
|
|
39
42
|
* @param {Object} params
|
|
40
43
|
* @param {string} params.sessionId - Session UUID
|
|
41
|
-
* @param {string} params.engine - 'claude'|'claude-code'|'codex'|'gemini'
|
|
44
|
+
* @param {string} params.engine - 'claude'|'claude-code'|'codex'|'gemini'|'qwen'
|
|
42
45
|
* @param {string} params.workspacePath - Workspace directory (required for Claude)
|
|
43
46
|
* @param {number} [params.limit=30] - Max messages to return
|
|
44
47
|
* @param {number} [params.before] - Timestamp cursor for pagination (ms)
|
|
@@ -76,6 +79,9 @@ class CliLoader {
|
|
|
76
79
|
case 'gemini':
|
|
77
80
|
result = await this.loadGeminiMessages({ sessionId, nativeId, limit, before, mode });
|
|
78
81
|
break;
|
|
82
|
+
case 'qwen':
|
|
83
|
+
result = await this.loadQwenMessages({ sessionId, nativeId, workspacePath, limit, before, mode });
|
|
84
|
+
break;
|
|
79
85
|
|
|
80
86
|
default:
|
|
81
87
|
throw new Error(`Unsupported engine: ${engine}`);
|
|
@@ -94,6 +100,7 @@ class CliLoader {
|
|
|
94
100
|
if (lower.includes('claude')) return 'claude';
|
|
95
101
|
if (lower.includes('codex') || lower.includes('openai')) return 'codex';
|
|
96
102
|
if (lower.includes('gemini') || lower.includes('google')) return 'gemini';
|
|
103
|
+
if (lower.includes('qwen')) return 'qwen';
|
|
97
104
|
return lower;
|
|
98
105
|
}
|
|
99
106
|
|
|
@@ -108,6 +115,14 @@ class CliLoader {
|
|
|
108
115
|
return workspacePath.replace(/[\/\.]/g, '-');
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Convert workspace path to Qwen project dir (matches Qwen Storage.sanitizeCwd)
|
|
120
|
+
*/
|
|
121
|
+
qwenPathToProject(workspacePath) {
|
|
122
|
+
if (!workspacePath) return 'default';
|
|
123
|
+
return workspacePath.replace(/[^a-zA-Z0-9]/g, '-');
|
|
124
|
+
}
|
|
125
|
+
|
|
111
126
|
// ============================================================
|
|
112
127
|
// CLAUDE - Load from ~/.claude/projects/<slug>/<sessionId>.jsonl
|
|
113
128
|
// ============================================================
|
|
@@ -368,6 +383,67 @@ class CliLoader {
|
|
|
368
383
|
return this._paginateMessages(messages, limit, before, mode);
|
|
369
384
|
}
|
|
370
385
|
|
|
386
|
+
// ============================================================
|
|
387
|
+
// QWEN - Load from ~/.qwen/projects/<sanitized>/chats/<sessionId>.jsonl
|
|
388
|
+
// ============================================================
|
|
389
|
+
|
|
390
|
+
async loadQwenMessages({ sessionId, nativeId, workspacePath, limit, before, mode }) {
|
|
391
|
+
if (!workspacePath) {
|
|
392
|
+
console.warn('[CliLoader] No workspacePath for Qwen, using cwd');
|
|
393
|
+
workspacePath = process.cwd();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const project = this.qwenPathToProject(workspacePath);
|
|
397
|
+
const fileId = nativeId || sessionId;
|
|
398
|
+
const sessionFile = path.join(this.qwenPath, 'projects', project, 'chats', `${fileId}.jsonl`);
|
|
399
|
+
|
|
400
|
+
if (!fs.existsSync(sessionFile)) {
|
|
401
|
+
console.log(`[CliLoader] Qwen session file not found: ${sessionFile}`);
|
|
402
|
+
return this._emptyResult();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const rawMessages = await this._parseJsonlFile(sessionFile);
|
|
406
|
+
|
|
407
|
+
const messages = rawMessages
|
|
408
|
+
.filter(entry => entry.type === 'user' || entry.type === 'assistant')
|
|
409
|
+
.map(entry => this._normalizeQwenEntry(entry));
|
|
410
|
+
|
|
411
|
+
return this._paginateMessages(messages, limit, before, mode);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Normalize Qwen session entry to message shape
|
|
416
|
+
*/
|
|
417
|
+
_normalizeQwenEntry(entry) {
|
|
418
|
+
const role = entry.type || 'assistant';
|
|
419
|
+
const created_at = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
|
|
420
|
+
|
|
421
|
+
let content = '';
|
|
422
|
+
const parts = entry.message?.parts;
|
|
423
|
+
if (Array.isArray(parts)) {
|
|
424
|
+
content = parts
|
|
425
|
+
.filter(p => p && p.text)
|
|
426
|
+
.map(p => p.text)
|
|
427
|
+
.join('\\n');
|
|
428
|
+
} else if (typeof entry.message?.content === 'string') {
|
|
429
|
+
content = entry.message.content;
|
|
430
|
+
} else if (entry.text) {
|
|
431
|
+
content = entry.text;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
id: entry.uuid || `qwen-${created_at}`,
|
|
436
|
+
role,
|
|
437
|
+
content,
|
|
438
|
+
engine: 'qwen',
|
|
439
|
+
created_at,
|
|
440
|
+
metadata: {
|
|
441
|
+
model: entry.model,
|
|
442
|
+
usage: entry.usageMetadata
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
371
447
|
/**
|
|
372
448
|
* Normalize Gemini session entry to message shape
|
|
373
449
|
*/
|
|
@@ -500,6 +576,10 @@ class CliLoader {
|
|
|
500
576
|
|
|
501
577
|
case 'gemini':
|
|
502
578
|
return path.join(this.geminiPath, 'sessions', `${sessionId}.jsonl`);
|
|
579
|
+
case 'qwen': {
|
|
580
|
+
const project = this.qwenPathToProject(workspacePath);
|
|
581
|
+
return path.join(this.qwenPath, 'projects', project, 'chats', `${sessionId}.jsonl`);
|
|
582
|
+
}
|
|
503
583
|
|
|
504
584
|
default:
|
|
505
585
|
return null;
|
|
@@ -21,7 +21,8 @@ const ENGINE_LIMITS = {
|
|
|
21
21
|
'claude': { maxTokens: 4000, preferSummary: true },
|
|
22
22
|
'codex': { maxTokens: 3000, preferSummary: true, codeOnly: true },
|
|
23
23
|
'deepseek': { maxTokens: 3000, preferSummary: true },
|
|
24
|
-
'gemini': { maxTokens: 6000, preferSummary: false } // Gemini has large context
|
|
24
|
+
'gemini': { maxTokens: 6000, preferSummary: false }, // Gemini has large context
|
|
25
|
+
'qwen': { maxTokens: 6000, preferSummary: false } // Qwen Coder large context
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
class ContextBridge {
|
|
@@ -152,7 +153,8 @@ class ContextBridge {
|
|
|
152
153
|
'claude': 'Claude Code (Anthropic)',
|
|
153
154
|
'codex': 'Codex (OpenAI)',
|
|
154
155
|
'gemini': 'Gemini (Google)',
|
|
155
|
-
'deepseek': 'DeepSeek'
|
|
156
|
+
'deepseek': 'DeepSeek',
|
|
157
|
+
'qwen': 'Qwen Code (Alibaba)'
|
|
156
158
|
};
|
|
157
159
|
|
|
158
160
|
const fromName = engineNames[fromEngine] || fromEngine;
|