@pixelbyte-software/pixcode 1.36.3 → 1.36.4
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/assets/index-CgF0-_6Z.css +32 -0
- package/dist/assets/{index-Bp8mXdQd.js → index-D-YjltED.js} +146 -146
- package/dist/index.html +2 -2
- package/dist-server/server/daemon-manager.js +18 -12
- package/dist-server/server/daemon-manager.js.map +1 -1
- package/dist-server/server/database/db.js +49 -0
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/index.js +8 -4
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +17 -3
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/routes/telegram.js +16 -2
- package/dist-server/server/routes/telegram.js.map +1 -1
- package/dist-server/server/services/telegram/bot.js +48 -6
- package/dist-server/server/services/telegram/bot.js.map +1 -1
- package/dist-server/server/services/telegram/control-center.js +761 -0
- package/dist-server/server/services/telegram/control-center.js.map +1 -0
- package/dist-server/server/services/telegram/telegram-http-client.js +26 -4
- package/dist-server/server/services/telegram/telegram-http-client.js.map +1 -1
- package/dist-server/server/services/telegram/translations.js +138 -2
- package/dist-server/server/services/telegram/translations.js.map +1 -1
- package/package.json +3 -1
- package/scripts/smoke/daemon-entrypoint.mjs +20 -0
- package/scripts/smoke/orchestration-user-facing-output.mjs +25 -0
- package/scripts/smoke/shell-manual-disconnect.mjs +30 -0
- package/scripts/smoke/side-panel-editor-layout.mjs +34 -0
- package/scripts/smoke/static-root-routing.mjs +21 -0
- package/scripts/smoke/telegram-control.mjs +242 -0
- package/scripts/smoke/version-modal-autoshow.mjs +29 -0
- package/server/daemon-manager.js +17 -12
- package/server/database/db.js +52 -0
- package/server/index.js +9 -5
- package/server/modules/orchestration/workflows/workflow-runner.ts +18 -3
- package/server/routes/telegram.js +17 -2
- package/server/services/telegram/bot.js +58 -6
- package/server/services/telegram/control-center.js +814 -0
- package/server/services/telegram/telegram-http-client.js +25 -4
- package/server/services/telegram/translations.js +138 -2
- package/dist/assets/index-Dx7QyTSN.css +0 -32
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const runtimeDir = path.resolve('.pixcode-dev', 'smoke-telegram-control');
|
|
8
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
9
|
+
process.env.DATABASE_PATH = path.join(runtimeDir, 'auth.db');
|
|
10
|
+
|
|
11
|
+
const checks = [
|
|
12
|
+
{
|
|
13
|
+
name: 'telegram bot wires the remote control center and callback queries',
|
|
14
|
+
file: 'server/services/telegram/bot.js',
|
|
15
|
+
test: (source) => (
|
|
16
|
+
source.includes('handleTelegramControlMessage')
|
|
17
|
+
&& source.includes('handleTelegramControlCallback')
|
|
18
|
+
&& source.includes("bot.on('callback_query'")
|
|
19
|
+
),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'telegram HTTP client polls callback queries and can answer them',
|
|
23
|
+
file: 'server/services/telegram/telegram-http-client.js',
|
|
24
|
+
test: (source) => (
|
|
25
|
+
source.includes("allowed_updates: ['message', 'callback_query']")
|
|
26
|
+
&& source.includes('answerCallbackQuery')
|
|
27
|
+
&& source.includes('editMessageText')
|
|
28
|
+
&& source.includes("this.emit('callback_query'")
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'telegram control center exposes provider, model, workflow, install, and settings actions',
|
|
33
|
+
file: 'server/services/telegram/control-center.js',
|
|
34
|
+
test: (source) => (
|
|
35
|
+
source.includes('showMainMenu')
|
|
36
|
+
&& source.includes('showProviderMenu')
|
|
37
|
+
&& source.includes('showModelMenu')
|
|
38
|
+
&& source.includes('showWorkflowMenu')
|
|
39
|
+
&& source.includes('runWorkflow')
|
|
40
|
+
&& source.includes('startCliInstall')
|
|
41
|
+
&& source.includes('updateTelegramControlState')
|
|
42
|
+
&& source.includes('/api/agent')
|
|
43
|
+
&& source.includes('/api/orchestration/workflows')
|
|
44
|
+
),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'telegram link state persists remote-control preferences',
|
|
48
|
+
file: 'server/database/db.js',
|
|
49
|
+
test: (source) => (
|
|
50
|
+
source.includes('telegram_control')
|
|
51
|
+
&& source.includes('getControlState')
|
|
52
|
+
&& source.includes('updateControlState')
|
|
53
|
+
&& source.includes('remoteControlEnabled')
|
|
54
|
+
),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'telegram settings UI exposes remote-control toggles',
|
|
58
|
+
file: 'src/components/settings/view/tabs/telegram-settings/TelegramSettingsTab.tsx',
|
|
59
|
+
test: (source) => (
|
|
60
|
+
source.includes('controlEnabled')
|
|
61
|
+
&& source.includes('progressMode')
|
|
62
|
+
&& source.includes('telegram.control.title')
|
|
63
|
+
&& source.includes('telegram.control.progressMode')
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const failures = [];
|
|
69
|
+
|
|
70
|
+
for (const check of checks) {
|
|
71
|
+
let source = '';
|
|
72
|
+
try {
|
|
73
|
+
source = readFileSync(check.file, 'utf8');
|
|
74
|
+
} catch {
|
|
75
|
+
failures.push(`${check.name} (${check.file} missing)`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!check.test(source)) failures.push(check.name);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const {
|
|
84
|
+
handleTelegramControlCallback,
|
|
85
|
+
handleTelegramControlMessage,
|
|
86
|
+
} = await import('../../server/services/telegram/control-center.js');
|
|
87
|
+
const {
|
|
88
|
+
handleIncomingTelegramMessage,
|
|
89
|
+
setTelegramBotForTesting,
|
|
90
|
+
} = await import('../../server/services/telegram/bot.js');
|
|
91
|
+
const { telegramLinksDb } = await import('../../server/database/db.js');
|
|
92
|
+
|
|
93
|
+
const userId = 4242;
|
|
94
|
+
telegramLinksDb.unlink(userId);
|
|
95
|
+
telegramLinksDb.setPairingCode(userId, '123456', new Date(Date.now() + 600_000).toISOString(), 'tr');
|
|
96
|
+
telegramLinksDb.verify(userId, 'chat-4242', 'ali');
|
|
97
|
+
|
|
98
|
+
const sent = [];
|
|
99
|
+
const bot = {
|
|
100
|
+
sendMessage: async (chatId, text, extra = {}) => {
|
|
101
|
+
sent.push({ chatId, text, extra });
|
|
102
|
+
return { ok: true };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const link = telegramLinksDb.getByUserId(userId);
|
|
107
|
+
const expectReply = async (input, expectedFragment) => {
|
|
108
|
+
sent.length = 0;
|
|
109
|
+
const handled = await handleTelegramControlMessage({
|
|
110
|
+
bot,
|
|
111
|
+
msg: { chat: { id: 4242 }, text: input },
|
|
112
|
+
link,
|
|
113
|
+
});
|
|
114
|
+
assert.equal(handled, true, `${input} should be handled`);
|
|
115
|
+
assert.ok(sent.length > 0, `${input} should send at least one reply`);
|
|
116
|
+
assert.ok(
|
|
117
|
+
sent[0].text.includes(expectedFragment),
|
|
118
|
+
`${input} should include "${expectedFragment}" but got "${sent[0].text}"`,
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
await expectReply('/start', 'Pixcode Telegram kontrol merkezi');
|
|
123
|
+
await expectReply('/help', 'Komutlar:');
|
|
124
|
+
await expectReply('/start@Otobot', 'Pixcode Telegram kontrol merkezi');
|
|
125
|
+
await expectReply('/help@Otobot', 'Komutlar:');
|
|
126
|
+
await expectReply('/', 'Komutlar:');
|
|
127
|
+
|
|
128
|
+
const menuEvents = [];
|
|
129
|
+
const menuBot = {
|
|
130
|
+
sendMessage: async (chatId, text, extra = {}) => {
|
|
131
|
+
menuEvents.push({ type: 'send', chatId, text, extra });
|
|
132
|
+
return { ok: true, message_id: 77 };
|
|
133
|
+
},
|
|
134
|
+
editMessageText: async (text, extra = {}) => {
|
|
135
|
+
menuEvents.push({ type: 'edit', text, extra });
|
|
136
|
+
return { ok: true, message_id: extra.message_id };
|
|
137
|
+
},
|
|
138
|
+
answerCallbackQuery: async () => ({ ok: true }),
|
|
139
|
+
};
|
|
140
|
+
const findButton = (markup, predicate) => {
|
|
141
|
+
for (const row of markup?.inline_keyboard || []) {
|
|
142
|
+
for (const candidate of row) {
|
|
143
|
+
if (predicate(candidate)) return candidate;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
menuEvents.length = 0;
|
|
150
|
+
await handleTelegramControlMessage({
|
|
151
|
+
bot: menuBot,
|
|
152
|
+
msg: { chat: { id: 4242 }, text: '/settings' },
|
|
153
|
+
link,
|
|
154
|
+
});
|
|
155
|
+
const settingsMenu = menuEvents.at(-1);
|
|
156
|
+
const languageButton = findButton(
|
|
157
|
+
settingsMenu.extra.reply_markup,
|
|
158
|
+
(candidate) => /language|dil/i.test(candidate.text),
|
|
159
|
+
);
|
|
160
|
+
assert.ok(languageButton, 'settings menu should expose a language button');
|
|
161
|
+
|
|
162
|
+
menuEvents.length = 0;
|
|
163
|
+
await handleTelegramControlCallback({
|
|
164
|
+
bot: menuBot,
|
|
165
|
+
query: {
|
|
166
|
+
id: 'query-language-menu',
|
|
167
|
+
data: languageButton.callback_data,
|
|
168
|
+
message: { chat: { id: 4242 }, message_id: 77 },
|
|
169
|
+
},
|
|
170
|
+
link,
|
|
171
|
+
});
|
|
172
|
+
assert.equal(menuEvents.length, 1, 'callback menus should replace the existing menu message');
|
|
173
|
+
assert.equal(menuEvents[0].type, 'edit', 'callback menus should use editMessageText');
|
|
174
|
+
const trButton = findButton(menuEvents[0].extra.reply_markup, (candidate) => candidate.text === 'tr');
|
|
175
|
+
assert.ok(trButton, 'language menu should include Turkish');
|
|
176
|
+
|
|
177
|
+
menuEvents.length = 0;
|
|
178
|
+
await handleTelegramControlCallback({
|
|
179
|
+
bot: menuBot,
|
|
180
|
+
query: {
|
|
181
|
+
id: 'query-language-tr',
|
|
182
|
+
data: trButton.callback_data,
|
|
183
|
+
message: { chat: { id: 4242 }, message_id: 77 },
|
|
184
|
+
},
|
|
185
|
+
link,
|
|
186
|
+
});
|
|
187
|
+
assert.equal(telegramLinksDb.getByUserId(userId).language, 'tr', 'language selection should persist');
|
|
188
|
+
assert.equal(menuEvents.length, 1, 'language selection should not send a confirmation plus a second menu');
|
|
189
|
+
assert.equal(menuEvents[0].type, 'edit', 'language selection should replace the menu in place');
|
|
190
|
+
assert.ok(
|
|
191
|
+
menuEvents[0].text.includes('Pixcode Telegram kontrol merkezi'),
|
|
192
|
+
`Turkish menu should render after selection, got "${menuEvents[0].text}"`,
|
|
193
|
+
);
|
|
194
|
+
assert.ok(
|
|
195
|
+
!menuEvents[0].text.includes('Project:'),
|
|
196
|
+
'Turkish menu should not keep English summary labels after language selection',
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const botLevelMessages = [];
|
|
200
|
+
setTelegramBotForTesting({
|
|
201
|
+
sendMessage: async (chatId, text, extra = {}) => {
|
|
202
|
+
botLevelMessages.push({ chatId, text, extra });
|
|
203
|
+
return { ok: true };
|
|
204
|
+
},
|
|
205
|
+
answerCallbackQuery: async () => ({ ok: true }),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const expectBotReply = async (input, expectedFragment) => {
|
|
209
|
+
botLevelMessages.length = 0;
|
|
210
|
+
await handleIncomingTelegramMessage({
|
|
211
|
+
chat: { id: 'chat-4242' },
|
|
212
|
+
text: input,
|
|
213
|
+
message_id: 1,
|
|
214
|
+
from: { username: 'ali' },
|
|
215
|
+
});
|
|
216
|
+
assert.ok(botLevelMessages.length > 0, `${input} should reply from bot.js`);
|
|
217
|
+
assert.ok(
|
|
218
|
+
botLevelMessages[0].text.includes(expectedFragment),
|
|
219
|
+
`${input} should include "${expectedFragment}" but got "${botLevelMessages[0].text}"`,
|
|
220
|
+
);
|
|
221
|
+
assert.ok(
|
|
222
|
+
!botLevelMessages.some((entry) => entry.text.includes('Mesaj son oturumuna iletildi')),
|
|
223
|
+
`${input} should never hit the bridge queue reply`,
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
await expectBotReply('/start', 'Nasıl başlarsın');
|
|
228
|
+
await expectBotReply('/help', 'Komutlar:');
|
|
229
|
+
|
|
230
|
+
telegramLinksDb.unlink(userId);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
failures.push(error?.message || String(error));
|
|
233
|
+
} finally {
|
|
234
|
+
rmSync(runtimeDir, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (failures.length > 0) {
|
|
238
|
+
console.error(`Telegram control smoke failed:\n- ${failures.join('\n- ')}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log('telegram control smoke passed');
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
const source = readFileSync('src/components/sidebar/view/Sidebar.tsx', 'utf8');
|
|
7
|
+
|
|
8
|
+
assert.ok(
|
|
9
|
+
source.includes('VERSION_RELEASE_NOTES_SEEN_KEY'),
|
|
10
|
+
'Sidebar should persist the latest equal-version release notes it auto-showed.',
|
|
11
|
+
);
|
|
12
|
+
assert.ok(
|
|
13
|
+
source.includes('localStorage.getItem(VERSION_RELEASE_NOTES_SEEN_KEY)'),
|
|
14
|
+
'Sidebar should read the seen release-notes version from localStorage.',
|
|
15
|
+
);
|
|
16
|
+
assert.ok(
|
|
17
|
+
source.includes('localStorage.setItem(VERSION_RELEASE_NOTES_SEEN_KEY, latestVersion)'),
|
|
18
|
+
'Sidebar should mark equal-version release notes as seen when auto-showing them.',
|
|
19
|
+
);
|
|
20
|
+
assert.ok(
|
|
21
|
+
source.includes('hasSeenCurrentReleaseNotes'),
|
|
22
|
+
'Sidebar should avoid auto-showing release notes when the current version was already seen.',
|
|
23
|
+
);
|
|
24
|
+
assert.ok(
|
|
25
|
+
source.includes('!hasSeenCurrentReleaseNotes'),
|
|
26
|
+
'The auto-show condition should be gated by the seen-version check.',
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
console.log('version modal autoshow smoke passed');
|
package/server/daemon-manager.js
CHANGED
|
@@ -95,15 +95,25 @@ function quoteSystemdArg(arg) {
|
|
|
95
95
|
return `"${String(arg).replace(/(["\\$`])/g, '\\$1')}"`;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
function resolveDaemonCliEntryPath(context = {}) {
|
|
98
|
+
export function resolveDaemonCliEntryPath(context = {}) {
|
|
99
99
|
const appRoot = context.appRoot || process.cwd();
|
|
100
100
|
const explicitCliEntry = context.cliEntry ? path.resolve(context.cliEntry) : null;
|
|
101
101
|
const argvCliEntry = process.argv[1] ? path.resolve(process.argv[1]) : null;
|
|
102
|
+
const distCliEntry = path.join(appRoot, 'dist-server', 'server', 'cli.js');
|
|
103
|
+
const sourceCliEntry = path.join(appRoot, 'server', 'cli.js');
|
|
104
|
+
const normalizeCliCandidate = (candidate) => {
|
|
105
|
+
if (!candidate) return null;
|
|
106
|
+
const resolved = path.resolve(candidate);
|
|
107
|
+
if (resolved === sourceCliEntry && fs.existsSync(distCliEntry)) {
|
|
108
|
+
return distCliEntry;
|
|
109
|
+
}
|
|
110
|
+
return resolved;
|
|
111
|
+
};
|
|
102
112
|
const candidatePaths = [
|
|
103
|
-
explicitCliEntry,
|
|
104
|
-
argvCliEntry,
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
normalizeCliCandidate(explicitCliEntry),
|
|
114
|
+
normalizeCliCandidate(argvCliEntry),
|
|
115
|
+
distCliEntry,
|
|
116
|
+
sourceCliEntry,
|
|
107
117
|
].filter(Boolean);
|
|
108
118
|
|
|
109
119
|
const existingPath = candidatePaths.find(candidate => fs.existsSync(candidate));
|
|
@@ -111,7 +121,7 @@ function resolveDaemonCliEntryPath(context = {}) {
|
|
|
111
121
|
return existingPath;
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
return explicitCliEntry || argvCliEntry ||
|
|
124
|
+
return normalizeCliCandidate(explicitCliEntry) || normalizeCliCandidate(argvCliEntry) || distCliEntry;
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
function hasPixcodeBinary() {
|
|
@@ -210,12 +220,7 @@ function parseDaemonArgs(args) {
|
|
|
210
220
|
|
|
211
221
|
function buildDaemonExecStart({ appRoot, serverPort, databasePath, nodeExecPath, cliEntry }) {
|
|
212
222
|
const nodeExec = nodeExecPath || process.execPath || 'node';
|
|
213
|
-
const
|
|
214
|
-
? path.resolve(cliEntry)
|
|
215
|
-
: (process.argv[1] ? path.resolve(process.argv[1]) : path.join(appRoot, 'dist-server', 'server', 'cli.js'));
|
|
216
|
-
const resolvedCliEntry = fs.existsSync(cliCandidate)
|
|
217
|
-
? cliCandidate
|
|
218
|
-
: path.join(appRoot, 'dist-server', 'server', 'cli.js');
|
|
223
|
+
const resolvedCliEntry = resolveDaemonCliEntryPath({ appRoot, cliEntry });
|
|
219
224
|
|
|
220
225
|
const args = [nodeExec, resolvedCliEntry, 'start', '--port', String(serverPort)];
|
|
221
226
|
if (databasePath) {
|
package/server/database/db.js
CHANGED
|
@@ -217,6 +217,7 @@ function migrateSqliteIfPresent() {
|
|
|
217
217
|
verified_at: tl.verified_at || null,
|
|
218
218
|
notifications_enabled: tl.notifications_enabled !== 0,
|
|
219
219
|
bridge_enabled: tl.bridge_enabled !== 0,
|
|
220
|
+
telegram_control: null,
|
|
220
221
|
updated_at: tl.updated_at || nowIso(),
|
|
221
222
|
});
|
|
222
223
|
}
|
|
@@ -672,6 +673,40 @@ const appConfigDb = {
|
|
|
672
673
|
// ---------------------------------------------------------------------------
|
|
673
674
|
// Telegram — singleton config + per-user links
|
|
674
675
|
// ---------------------------------------------------------------------------
|
|
676
|
+
const DEFAULT_TELEGRAM_CONTROL_STATE = {
|
|
677
|
+
remoteControlEnabled: true,
|
|
678
|
+
progressMode: 'final',
|
|
679
|
+
selectedProjectName: null,
|
|
680
|
+
selectedProjectPath: null,
|
|
681
|
+
selectedProvider: 'opencode',
|
|
682
|
+
selectedModel: null,
|
|
683
|
+
selectedWorkflowId: null,
|
|
684
|
+
awaiting: null,
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
function normalizeTelegramControlState(value = {}) {
|
|
688
|
+
const raw = value && typeof value === 'object' ? value : {};
|
|
689
|
+
const selectedProvider = ['claude', 'cursor', 'codex', 'gemini', 'qwen', 'opencode'].includes(raw.selectedProvider)
|
|
690
|
+
? raw.selectedProvider
|
|
691
|
+
: DEFAULT_TELEGRAM_CONTROL_STATE.selectedProvider;
|
|
692
|
+
const progressMode = ['final', 'steps', 'all'].includes(raw.progressMode)
|
|
693
|
+
? raw.progressMode
|
|
694
|
+
: DEFAULT_TELEGRAM_CONTROL_STATE.progressMode;
|
|
695
|
+
|
|
696
|
+
return {
|
|
697
|
+
...DEFAULT_TELEGRAM_CONTROL_STATE,
|
|
698
|
+
...raw,
|
|
699
|
+
remoteControlEnabled: raw.remoteControlEnabled !== false,
|
|
700
|
+
progressMode,
|
|
701
|
+
selectedProvider,
|
|
702
|
+
selectedProjectName: typeof raw.selectedProjectName === 'string' ? raw.selectedProjectName : null,
|
|
703
|
+
selectedProjectPath: typeof raw.selectedProjectPath === 'string' ? raw.selectedProjectPath : null,
|
|
704
|
+
selectedModel: typeof raw.selectedModel === 'string' ? raw.selectedModel : null,
|
|
705
|
+
selectedWorkflowId: typeof raw.selectedWorkflowId === 'string' ? raw.selectedWorkflowId : null,
|
|
706
|
+
awaiting: raw.awaiting && typeof raw.awaiting === 'object' ? raw.awaiting : null,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
675
710
|
const telegramConfigDb = {
|
|
676
711
|
get: () => {
|
|
677
712
|
const row = store.raw.telegram_config[0];
|
|
@@ -705,6 +740,7 @@ const telegramLinksDb = {
|
|
|
705
740
|
verified_at: null,
|
|
706
741
|
notifications_enabled: true,
|
|
707
742
|
bridge_enabled: true,
|
|
743
|
+
telegram_control: normalizeTelegramControlState(),
|
|
708
744
|
updated_at: nowIso(),
|
|
709
745
|
});
|
|
710
746
|
},
|
|
@@ -742,6 +778,7 @@ const telegramLinksDb = {
|
|
|
742
778
|
language: row.language,
|
|
743
779
|
notifications_enabled: row.notifications_enabled,
|
|
744
780
|
bridge_enabled: row.bridge_enabled,
|
|
781
|
+
telegram_control: normalizeTelegramControlState(row.telegram_control),
|
|
745
782
|
};
|
|
746
783
|
},
|
|
747
784
|
listVerified: () =>
|
|
@@ -752,6 +789,7 @@ const telegramLinksDb = {
|
|
|
752
789
|
language: r.language,
|
|
753
790
|
notifications_enabled: r.notifications_enabled,
|
|
754
791
|
bridge_enabled: r.bridge_enabled,
|
|
792
|
+
telegram_control: normalizeTelegramControlState(r.telegram_control),
|
|
755
793
|
})),
|
|
756
794
|
updatePreferences: (userId, { language, notificationsEnabled, bridgeEnabled }) => {
|
|
757
795
|
const patch = { updated_at: nowIso() };
|
|
@@ -761,6 +799,20 @@ const telegramLinksDb = {
|
|
|
761
799
|
if (Object.keys(patch).length === 1) return; // only updated_at → no real change
|
|
762
800
|
store.updateWhere('telegram_links', (r) => r.user_id === userId, patch);
|
|
763
801
|
},
|
|
802
|
+
getControlState: (userId) => {
|
|
803
|
+
const row = store.findWhere('telegram_links', (r) => r.user_id === userId);
|
|
804
|
+
return normalizeTelegramControlState(row?.telegram_control);
|
|
805
|
+
},
|
|
806
|
+
updateControlState: (userId, patch) => {
|
|
807
|
+
const row = store.findWhere('telegram_links', (r) => r.user_id === userId);
|
|
808
|
+
const current = normalizeTelegramControlState(row?.telegram_control);
|
|
809
|
+
const next = normalizeTelegramControlState({ ...current, ...(patch || {}) });
|
|
810
|
+
store.updateWhere('telegram_links', (r) => r.user_id === userId, {
|
|
811
|
+
telegram_control: next,
|
|
812
|
+
updated_at: nowIso(),
|
|
813
|
+
});
|
|
814
|
+
return next;
|
|
815
|
+
},
|
|
764
816
|
unlink: (userId) => {
|
|
765
817
|
store.deleteWhere('telegram_links', (r) => r.user_id === userId);
|
|
766
818
|
},
|
package/server/index.js
CHANGED
|
@@ -413,11 +413,9 @@ app.use('/api/telegram', authenticateToken, telegramRoutes);
|
|
|
413
413
|
// Agent API Routes (uses API key authentication)
|
|
414
414
|
app.use('/api/agent', agentRoutes);
|
|
415
415
|
|
|
416
|
-
//
|
|
417
|
-
app
|
|
418
|
-
|
|
419
|
-
// Static files served after API routes
|
|
420
|
-
// Add cache control: HTML files should not be cached, but assets can be cached
|
|
416
|
+
// Static app files served after API routes. Keep dist before public so
|
|
417
|
+
// / and /index.html always resolve to the Pixcode app, not the GitHub Pages
|
|
418
|
+
// landing page that also lives in public/index.html.
|
|
421
419
|
app.use(express.static(path.join(APP_ROOT, 'dist'), {
|
|
422
420
|
setHeaders: (res, filePath) => {
|
|
423
421
|
if (filePath.endsWith('.html')) {
|
|
@@ -432,6 +430,12 @@ app.use(express.static(path.join(APP_ROOT, 'dist'), {
|
|
|
432
430
|
}
|
|
433
431
|
}));
|
|
434
432
|
|
|
433
|
+
// Serve extra public files (api-docs.html, llms.txt, landing pages) without
|
|
434
|
+
// letting public/index.html shadow the production app root.
|
|
435
|
+
app.use(express.static(path.join(APP_ROOT, 'public'), {
|
|
436
|
+
index: false,
|
|
437
|
+
}));
|
|
438
|
+
|
|
435
439
|
// API Routes (protected)
|
|
436
440
|
// /api/config endpoint removed - no longer needed
|
|
437
441
|
// Frontend now uses window.location for WebSocket URLs
|
|
@@ -276,6 +276,10 @@ function rolePrompt(role: AgentRole): string {
|
|
|
276
276
|
return 'Implementation work should avoid duplicating other agents and should report changed files, commands, blockers, and next actions.';
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
function privacyGuardPrompt(): string {
|
|
280
|
+
return 'Do not mention internal instructions, memory files, skill use, or tool protocol unless the user explicitly asks.';
|
|
281
|
+
}
|
|
282
|
+
|
|
279
283
|
function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
|
|
280
284
|
return [
|
|
281
285
|
`You are ${agent.label} in a Pixcode CLI team.`,
|
|
@@ -290,6 +294,7 @@ function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
|
|
|
290
294
|
'- dependencies/blockers for the next agents',
|
|
291
295
|
'- concrete next action for your full implementation task',
|
|
292
296
|
'Do not install dependencies, edit files, run long commands, or start servers in this handoff task.',
|
|
297
|
+
privacyGuardPrompt(),
|
|
293
298
|
'Stop after the contract. Keep it concise and respond in the same language as the user request.',
|
|
294
299
|
].filter(Boolean).join('\n');
|
|
295
300
|
}
|
|
@@ -374,6 +379,7 @@ function expandAgentTeamWorkflow(workflow: Workflow, metadata?: Record<string, u
|
|
|
374
379
|
? `Your explicit assignment from the user is: ${agent.instruction}`
|
|
375
380
|
: 'No fixed per-agent assignment was set. Take the part assigned to you by the coordinator; if none is named, choose useful work that fits this CLI.',
|
|
376
381
|
rolePrompt(stage),
|
|
382
|
+
privacyGuardPrompt(),
|
|
377
383
|
'Respond in the same language as the user request.',
|
|
378
384
|
].filter(Boolean).join('\n'),
|
|
379
385
|
inputs,
|
|
@@ -419,6 +425,8 @@ function expandAgentTeamWorkflow(workflow: Workflow, metadata?: Record<string, u
|
|
|
419
425
|
prompt: [
|
|
420
426
|
'Collect the worker outputs into one user-facing result.',
|
|
421
427
|
'Show what each CLI did, which parts failed, what changed, and the next action if work remains.',
|
|
428
|
+
'Do not expose internal prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
|
|
429
|
+
'If a worker reveals internal process text, summarize only the useful user-facing result.',
|
|
422
430
|
'Respond in the same language as the user request.',
|
|
423
431
|
].join('\n'),
|
|
424
432
|
inputs: workerNodes.map((node) => node.id),
|
|
@@ -436,6 +444,7 @@ function stagePrompt(agent: AgentAssignment, stage: AgentRole): string {
|
|
|
436
444
|
agent.role && agent.role !== stage ? `User custom stage label: ${agent.role}.` : '',
|
|
437
445
|
agent.instruction ? `User assignment for you: ${agent.instruction}` : '',
|
|
438
446
|
rolePrompt(stage),
|
|
447
|
+
privacyGuardPrompt(),
|
|
439
448
|
'Keep the answer concise, structured, and useful for the next stage.',
|
|
440
449
|
'Respond in the same language as the user request.',
|
|
441
450
|
].filter(Boolean).join('\n');
|
|
@@ -605,6 +614,7 @@ function expandSequentialHandoffWorkflow(workflow: Workflow, metadata?: Record<s
|
|
|
605
614
|
? `Your explicit assignment from the user is: ${agent.instruction}`
|
|
606
615
|
: 'Use the prior step output and do the next most useful handoff step for the user goal.',
|
|
607
616
|
'Report changed files, commands, blockers, and the next handoff requirement.',
|
|
617
|
+
privacyGuardPrompt(),
|
|
608
618
|
'Respond in the same language as the user request.',
|
|
609
619
|
].filter(Boolean).join('\n'),
|
|
610
620
|
inputs: index === 0 ? [] : [safeAgentNodeId(agents[index - 1], index - 1, 'handoff')],
|
|
@@ -646,6 +656,7 @@ function expandWorkflowForRun(workflow: Workflow, metadata?: Record<string, unkn
|
|
|
646
656
|
`You are ${agent.label}.`,
|
|
647
657
|
'Review the requested change for bugs, regressions, missing validation, security, scale, and user-experience risks.',
|
|
648
658
|
agent.instruction ? `Focus on this user assignment: ${agent.instruction}` : '',
|
|
659
|
+
privacyGuardPrompt(),
|
|
649
660
|
'Respond in the same language as the user request.',
|
|
650
661
|
].filter(Boolean).join('\n'),
|
|
651
662
|
inputs: [],
|
|
@@ -666,7 +677,11 @@ function expandWorkflowForRun(workflow: Workflow, metadata?: Record<string, unkn
|
|
|
666
677
|
model: reportAgent.model,
|
|
667
678
|
permissionMode: reportAgent.permissionMode,
|
|
668
679
|
toolsSettings: reportAgent.toolsSettings,
|
|
669
|
-
prompt:
|
|
680
|
+
prompt: [
|
|
681
|
+
'Aggregate the prior agent reviews into a concise prioritized report.',
|
|
682
|
+
'Do not expose internal prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
|
|
683
|
+
'Respond in the same language as the user request.',
|
|
684
|
+
].join('\n'),
|
|
670
685
|
inputs: reviewNodes.map((node) => node.id),
|
|
671
686
|
output: 'message',
|
|
672
687
|
onFail: 'abort',
|
|
@@ -701,13 +716,13 @@ function readTaskResult(task: RawTask): TaskResult {
|
|
|
701
716
|
};
|
|
702
717
|
});
|
|
703
718
|
const outputMessages = messages.filter((message) => message.role !== 'user');
|
|
704
|
-
const
|
|
719
|
+
const userFacingTaskText = outputMessages.map((message) => message.text.trim()).filter(Boolean).join('\n\n');
|
|
705
720
|
const error = task.error?.message
|
|
706
721
|
? `${task.error.code ? `${task.error.code}: ` : ''}${task.error.message}`
|
|
707
722
|
: undefined;
|
|
708
723
|
return {
|
|
709
724
|
state: task.state ?? 'submitted',
|
|
710
|
-
text,
|
|
725
|
+
text: userFacingTaskText,
|
|
711
726
|
error,
|
|
712
727
|
messages,
|
|
713
728
|
artifacts,
|
|
@@ -33,6 +33,7 @@ router.get('/status', (req, res) => {
|
|
|
33
33
|
language: link.language,
|
|
34
34
|
notificationsEnabled: Boolean(link.notifications_enabled),
|
|
35
35
|
bridgeEnabled: Boolean(link.bridge_enabled),
|
|
36
|
+
control: telegramLinksDb.getControlState(req.user.id),
|
|
36
37
|
pairingCode: link.pairing_code,
|
|
37
38
|
pairingExpiresAt: link.pairing_code_expires_at,
|
|
38
39
|
verifiedAt: link.verified_at,
|
|
@@ -98,13 +99,27 @@ router.post('/pairing-code', (req, res) => {
|
|
|
98
99
|
// PATCH /api/telegram/link — update language / toggles on the user's link
|
|
99
100
|
router.patch('/link', (req, res) => {
|
|
100
101
|
try {
|
|
101
|
-
const { language, notificationsEnabled, bridgeEnabled } = req.body || {};
|
|
102
|
+
const { language, notificationsEnabled, bridgeEnabled, controlEnabled, progressMode } = req.body || {};
|
|
102
103
|
const payload = {};
|
|
103
104
|
if (language !== undefined) payload.language = sanitizeLanguage(language);
|
|
104
105
|
if (notificationsEnabled !== undefined) payload.notificationsEnabled = Boolean(notificationsEnabled);
|
|
105
106
|
if (bridgeEnabled !== undefined) payload.bridgeEnabled = Boolean(bridgeEnabled);
|
|
106
107
|
telegramLinksDb.updatePreferences(req.user.id, payload);
|
|
107
|
-
|
|
108
|
+
|
|
109
|
+
const controlPatch = {};
|
|
110
|
+
if (controlEnabled !== undefined) controlPatch.remoteControlEnabled = Boolean(controlEnabled);
|
|
111
|
+
if (progressMode !== undefined && ['final', 'steps', 'all'].includes(progressMode)) {
|
|
112
|
+
controlPatch.progressMode = progressMode;
|
|
113
|
+
}
|
|
114
|
+
if (Object.keys(controlPatch).length > 0) {
|
|
115
|
+
telegramLinksDb.updateControlState(req.user.id, controlPatch);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
res.json({
|
|
119
|
+
success: true,
|
|
120
|
+
link: telegramLinksDb.getByUserId(req.user.id),
|
|
121
|
+
control: telegramLinksDb.getControlState(req.user.id),
|
|
122
|
+
});
|
|
108
123
|
} catch (error) {
|
|
109
124
|
console.error('telegram/link patch failed:', error);
|
|
110
125
|
res.status(500).json({ error: 'Failed to update link' });
|