@mmmbuto/nexuscli 0.9.7-termux → 0.9.7001-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.
|
@@ -11,6 +11,7 @@ const { v4: uuidv4 } = require('uuid');
|
|
|
11
11
|
const sessionManager = require('../services/session-manager');
|
|
12
12
|
const contextBridge = require('../services/context-bridge');
|
|
13
13
|
const { resolveWorkspacePath } = require('../../utils/workspace');
|
|
14
|
+
const { normalizeAttachments } = require('../../utils/attachments');
|
|
14
15
|
|
|
15
16
|
const router = express.Router();
|
|
16
17
|
const qwenWrapper = new QwenWrapper();
|
|
@@ -44,7 +45,7 @@ router.post('/', async (req, res) => {
|
|
|
44
45
|
console.log('[Qwen] === NEW QWEN REQUEST ===');
|
|
45
46
|
console.log('[Qwen] Body:', JSON.stringify(req.body, null, 2));
|
|
46
47
|
|
|
47
|
-
const { conversationId, message, model = 'coder-model', workspace } = req.body;
|
|
48
|
+
const { conversationId, message, model = 'coder-model', workspace, attachments } = req.body;
|
|
48
49
|
|
|
49
50
|
if (!message) {
|
|
50
51
|
return res.status(400).json({ error: 'message required' });
|
|
@@ -63,6 +64,13 @@ router.post('/', async (req, res) => {
|
|
|
63
64
|
console.warn(`[Qwen] Workspace corrected: ${workspace} → ${workspacePath}`);
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
const attachmentInfo = normalizeAttachments({
|
|
68
|
+
message,
|
|
69
|
+
attachments,
|
|
70
|
+
workspacePath
|
|
71
|
+
});
|
|
72
|
+
const userMessage = attachmentInfo.promptMessage || message;
|
|
73
|
+
|
|
66
74
|
const frontendConversationId = conversationId || uuidv4();
|
|
67
75
|
ensureConversation(frontendConversationId, workspacePath);
|
|
68
76
|
|
|
@@ -91,7 +99,7 @@ router.post('/', async (req, res) => {
|
|
|
91
99
|
const lastEngine = Message.getLastEngine(frontendConversationId);
|
|
92
100
|
const isEngineBridge = lastEngine && lastEngine !== 'qwen';
|
|
93
101
|
|
|
94
|
-
let promptToSend =
|
|
102
|
+
let promptToSend = userMessage;
|
|
95
103
|
|
|
96
104
|
if (isEngineBridge) {
|
|
97
105
|
const contextResult = await contextBridge.buildContext({
|
|
@@ -99,7 +107,7 @@ router.post('/', async (req, res) => {
|
|
|
99
107
|
sessionId,
|
|
100
108
|
fromEngine: lastEngine,
|
|
101
109
|
toEngine: 'qwen',
|
|
102
|
-
userMessage:
|
|
110
|
+
userMessage: userMessage
|
|
103
111
|
});
|
|
104
112
|
promptToSend = contextResult.prompt;
|
|
105
113
|
|
|
@@ -113,6 +121,15 @@ router.post('/', async (req, res) => {
|
|
|
113
121
|
console.log(`[Qwen] Native resume: qwen --resume ${nativeSessionId}`);
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
const attachmentRefs = attachmentInfo.attachmentPaths.map((filePath) => {
|
|
125
|
+
const escapedPath = filePath.replace(/([\\\s,;!?()[\]{}])/g, '\\$1');
|
|
126
|
+
return `@${escapedPath}`;
|
|
127
|
+
});
|
|
128
|
+
if (attachmentRefs.length > 0) {
|
|
129
|
+
promptToSend = `${attachmentRefs.join('\n')}\n\n${promptToSend}`;
|
|
130
|
+
console.log(`[Qwen] Attached ${attachmentRefs.length} file(s) to prompt`);
|
|
131
|
+
}
|
|
132
|
+
|
|
116
133
|
// Save user message
|
|
117
134
|
try {
|
|
118
135
|
Message.create(
|
|
@@ -138,6 +155,7 @@ router.post('/', async (req, res) => {
|
|
|
138
155
|
threadId: nativeSessionId,
|
|
139
156
|
model,
|
|
140
157
|
workspacePath,
|
|
158
|
+
includeDirectories: attachmentInfo.includeDirectories,
|
|
141
159
|
processId: sessionId,
|
|
142
160
|
onStatus: (event) => {
|
|
143
161
|
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
@@ -83,6 +83,7 @@ class QwenWrapper extends BaseCliWrapper {
|
|
|
83
83
|
threadId,
|
|
84
84
|
model = DEFAULT_MODEL,
|
|
85
85
|
workspacePath,
|
|
86
|
+
includeDirectories = [],
|
|
86
87
|
onStatus,
|
|
87
88
|
processId: processIdOverride
|
|
88
89
|
}) {
|
|
@@ -99,6 +100,14 @@ class QwenWrapper extends BaseCliWrapper {
|
|
|
99
100
|
'--include-partial-messages',
|
|
100
101
|
];
|
|
101
102
|
|
|
103
|
+
if (includeDirectories && includeDirectories.length > 0) {
|
|
104
|
+
includeDirectories.forEach((dir) => {
|
|
105
|
+
if (dir) {
|
|
106
|
+
args.push('--include-directories', dir);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
if (threadId) {
|
|
103
112
|
args.push('--resume', threadId);
|
|
104
113
|
}
|
|
@@ -109,6 +118,9 @@ class QwenWrapper extends BaseCliWrapper {
|
|
|
109
118
|
console.log(`[QwenWrapper] ThreadId: ${threadId || '(new session)'}`);
|
|
110
119
|
console.log(`[QwenWrapper] CWD: ${cwd}`);
|
|
111
120
|
console.log(`[QwenWrapper] Prompt length: ${prompt.length}`);
|
|
121
|
+
if (includeDirectories && includeDirectories.length > 0) {
|
|
122
|
+
console.log(`[QwenWrapper] Include dirs: ${includeDirectories.join(', ')}`);
|
|
123
|
+
}
|
|
112
124
|
|
|
113
125
|
let ptyProcess;
|
|
114
126
|
try {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { fixTermuxPath } = require('./workspace');
|
|
4
|
+
|
|
5
|
+
const ATTACHMENT_LINE_REGEX = /^\s*\[Attached:\s*(.+?)\s*\]\s*$/;
|
|
6
|
+
const IMAGE_EXTS = new Set([
|
|
7
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff', '.tif', '.svg', '.heic'
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function parseAttachmentMarkers(message = '') {
|
|
11
|
+
if (!message) {
|
|
12
|
+
return { cleanMessage: message, paths: [] };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const lines = message.split('\n');
|
|
16
|
+
const paths = [];
|
|
17
|
+
const kept = [];
|
|
18
|
+
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const match = line.match(ATTACHMENT_LINE_REGEX);
|
|
21
|
+
if (match) {
|
|
22
|
+
paths.push(match[1]);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
kept.push(line);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const cleanMessage = kept.join('\n').trim();
|
|
29
|
+
return { cleanMessage, paths };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isImageAttachment(attachment) {
|
|
33
|
+
const mimeType = attachment?.mimeType || '';
|
|
34
|
+
if (mimeType.startsWith('image/')) return true;
|
|
35
|
+
const ext = path.extname(attachment?.path || '').toLowerCase();
|
|
36
|
+
return IMAGE_EXTS.has(ext);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeAttachmentPath(rawPath, workspacePath) {
|
|
40
|
+
if (!rawPath || typeof rawPath !== 'string') return null;
|
|
41
|
+
let fixedPath = fixTermuxPath(rawPath);
|
|
42
|
+
if (!path.isAbsolute(fixedPath) && workspacePath) {
|
|
43
|
+
fixedPath = path.resolve(workspacePath, fixedPath);
|
|
44
|
+
}
|
|
45
|
+
return fixedPath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeAttachments({ message, rawMessage, attachments, workspacePath }) {
|
|
49
|
+
const parsed = parseAttachmentMarkers(message || '');
|
|
50
|
+
const promptMessage = rawMessage != null ? rawMessage : (parsed.cleanMessage || message || '');
|
|
51
|
+
|
|
52
|
+
const incoming = [];
|
|
53
|
+
if (Array.isArray(attachments)) {
|
|
54
|
+
incoming.push(...attachments);
|
|
55
|
+
}
|
|
56
|
+
if (parsed.paths.length > 0) {
|
|
57
|
+
parsed.paths.forEach((p) => incoming.push({ path: p }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
const normalized = [];
|
|
62
|
+
|
|
63
|
+
for (const entry of incoming) {
|
|
64
|
+
if (!entry) continue;
|
|
65
|
+
const rawPath = typeof entry === 'string' ? entry : entry.path;
|
|
66
|
+
const resolvedPath = normalizeAttachmentPath(rawPath, workspacePath);
|
|
67
|
+
if (!resolvedPath) continue;
|
|
68
|
+
if (!fs.existsSync(resolvedPath)) continue;
|
|
69
|
+
if (seen.has(resolvedPath)) continue;
|
|
70
|
+
|
|
71
|
+
seen.add(resolvedPath);
|
|
72
|
+
const record = {
|
|
73
|
+
...entry,
|
|
74
|
+
path: resolvedPath,
|
|
75
|
+
name: entry.name || path.basename(resolvedPath)
|
|
76
|
+
};
|
|
77
|
+
normalized.push(record);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const attachmentPaths = normalized.map((att) => att.path);
|
|
81
|
+
const includeDirectories = Array.from(
|
|
82
|
+
new Set(attachmentPaths.map((p) => path.dirname(p)))
|
|
83
|
+
);
|
|
84
|
+
const imageFiles = normalized.filter(isImageAttachment).map((att) => att.path);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
promptMessage,
|
|
88
|
+
attachments: normalized,
|
|
89
|
+
attachmentPaths,
|
|
90
|
+
includeDirectories,
|
|
91
|
+
imageFiles,
|
|
92
|
+
cleanMessage: parsed.cleanMessage
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
normalizeAttachments,
|
|
98
|
+
parseAttachmentMarkers,
|
|
99
|
+
isImageAttachment
|
|
100
|
+
};
|