@researai/deepscientist 1.5.13 → 1.5.14
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/docs/en/05_TUI_GUIDE.md +466 -96
- package/docs/en/README.md +2 -0
- package/docs/zh/05_TUI_GUIDE.md +465 -82
- package/docs/zh/README.md +2 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/artifact/service.py +125 -2
- package/src/deepscientist/connector/lingzhu_support.py +23 -4
- package/src/deepscientist/daemon/app.py +111 -30
- package/src/deepscientist/mcp/server.py +161 -19
- package/src/deepscientist/prompts/builder.py +13 -54
- package/src/deepscientist/quest/service.py +99 -0
- package/src/deepscientist/quest/stage_views.py +134 -29
- package/src/deepscientist/shared.py +6 -1
- package/src/prompts/system.md +220 -2065
- package/src/skills/baseline/SKILL.md +265 -994
- package/src/skills/baseline/references/baseline-checklist-template.md +21 -32
- package/src/skills/baseline/references/baseline-plan-template.md +41 -57
- package/src/tui/dist/app/AppContainer.js +1442 -52
- package/src/tui/dist/components/Composer.js +1 -1
- package/src/tui/dist/components/ConfigScreen.js +190 -36
- package/src/tui/dist/components/GradientStatusText.js +1 -20
- package/src/tui/dist/components/InputPrompt.js +41 -32
- package/src/tui/dist/components/LoadingIndicator.js +1 -1
- package/src/tui/dist/components/Logo.js +61 -38
- package/src/tui/dist/components/MainContent.js +10 -3
- package/src/tui/dist/components/WelcomePanel.js +4 -12
- package/src/tui/dist/components/messages/AssistantMessage.js +1 -1
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -3
- package/src/tui/dist/components/messages/OperationMessage.js +1 -1
- package/src/tui/dist/index.js +28 -1
- package/src/tui/dist/layouts/DefaultAppLayout.js +3 -3
- package/src/tui/dist/lib/api.js +17 -0
- package/src/tui/dist/lib/connectors.js +261 -0
- package/src/tui/dist/semantic-colors.js +29 -19
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-CnJcXynW.js → AiManusChatView-DaF9Nge_.js} +12 -12
- package/src/ui/dist/assets/{AnalysisPlugin-DeyzPEhV.js → AnalysisPlugin-BSVx6dXE.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-CB1YODQn.js → CliPlugin-C9gzJX41.js} +9 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-B-xicq1e.js → CodeEditorPlugin-DU9G0Tox.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DT54ysXa.js → CodeViewerPlugin-DoX_fI9l.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-DQtKT-VD.js → DocViewerPlugin-C4FWIXuU.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-hqHbCfnv.js → GitDiffViewerPlugin-BgfFMgtf.js} +20 -20
- package/src/ui/dist/assets/{ImageViewerPlugin-OcVo33jV.js → ImageViewerPlugin-tcPkfY_x.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-DdGwhEUV.js → LabCopilotPanel-_dKV60Bf.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-Ciz1gDaX.js → LabPlugin-Bje0ayoC.js} +2 -2
- package/src/ui/dist/assets/{LatexPlugin-BhmjNQRC.js → LatexPlugin-CVsBzAln.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-BzdVH9Bx.js → MarkdownViewerPlugin-xjmrqv_8.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DmyHspXt.js → MarketplacePlugin-mMM2A8wP.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-BTVYRGkm.js → NotebookEditor-3kVDSOBo.js} +11 -11
- package/src/ui/dist/assets/{NotebookEditor-BMXKrDRk.js → NotebookEditor-SoJ8X-MO.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-CvcjJHXv.js → PdfLoader-DElVuHl9.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DW2ej8Vk.js → PdfMarkdownPlugin-Bq88XT4G.js} +2 -2
- package/src/ui/dist/assets/{PdfViewerPlugin-CmlDxbhU.js → PdfViewerPlugin-CsCXMo9S.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-DAjQZPSv.js → SearchPlugin-oUPvy19k.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-C-nVAZb_.js → TextViewerPlugin-CRkT9yNy.js} +5 -5
- package/src/ui/dist/assets/{VNCViewer-D7-dIYon.js → VNCViewer-BgbuvWhR.js} +10 -10
- package/src/ui/dist/assets/{bot-C_G4WtNI.js → bot-v_RASACv.js} +1 -1
- package/src/ui/dist/assets/{code-Cd7WfiWq.js → code-5hC9d0VH.js} +1 -1
- package/src/ui/dist/assets/{file-content-B57zsL9y.js → file-content-D1PxfOrp.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DVoheLFq.js → file-diff-panel-DG1oT_Hj.js} +1 -1
- package/src/ui/dist/assets/{file-socket-B5kXFxZP.js → file-socket-BmdFYQlk.js} +1 -1
- package/src/ui/dist/assets/{image-LLOjkMHF.js → image-Dqe2X2tW.js} +1 -1
- package/src/ui/dist/assets/{index-Dxa2eYMY.js → index-DVsMKK_y.js} +1 -1
- package/src/ui/dist/assets/{index-C3r2iGrp.js → index-Duvz8Ip0.js} +12 -12
- package/src/ui/dist/assets/{index-CLQauncb.js → index-Nt9hS4ck.js} +470 -165
- package/src/ui/dist/assets/{index-hOUOWbW2.js → index-RDlNXXx1.js} +2 -2
- package/src/ui/dist/assets/{monaco-BGGAEii3.js → monaco-DIXge1CP.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DlEr1_y5.js → pdf-effect-queue-BBTTQaO-.js} +1 -1
- package/src/ui/dist/assets/{popover-CWJbJuYY.js → popover-BWlolyxo.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CRJiucYO.js → project-sync-BM5PkFH4.js} +1 -1
- package/src/ui/dist/assets/{select-CoHB7pvH.js → select-D4dAtrA8.js} +2 -2
- package/src/ui/dist/assets/{sigma-D5aJWR8J.js → sigma-CKbE5jJT.js} +1 -1
- package/src/ui/dist/assets/{square-check-big-DUK_mnkS.js → square-check-big-CZNGMgiB.js} +1 -1
- package/src/ui/dist/assets/{trash-ChU3SEE3.js → trash-DaB37xAz.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-BrJBV3tY.js → useCliAccess-C2OmAcWe.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-C2OQaVWc.js → useFileDiffOverlay-Dowd1Ij4.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-C7Qqh-om.js → wrap-text-BGjAhAUq.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-rtX0FKya.js → zoom-out-dMZQMXzc.js} +1 -1
- package/src/ui/dist/index.html +1 -1
- package/uv.lock +1 -1
|
@@ -3,9 +3,29 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { useApp, useInput } from 'ink';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { client } from '../lib/api.js';
|
|
6
|
+
import { CONNECTOR_ORDER, connectorLabel, connectorSubtitle, createLingzhuAk, looksLikeWeixinQrImageUrl, resolveLingzhuAuthAk, supportsGuidedConnector, } from '../lib/connectorConfig.js';
|
|
7
|
+
import { connectorTargetLabel, normalizeConnectorTargets, qqProfileDisplayLabel, qqProfileStatus, selectQqProfileTarget, } from '../lib/connectors.js';
|
|
8
|
+
import { renderQrAscii } from '../lib/qr.js';
|
|
6
9
|
import { buildToolOperationContent, extractToolSubject } from '../lib/toolOperations.js';
|
|
7
10
|
import { DefaultAppLayout } from '../layouts/DefaultAppLayout.js';
|
|
8
11
|
const LOCAL_USER_SOURCE = 'tui-local';
|
|
12
|
+
const CONFIG_ROOT_ENTRIES = [
|
|
13
|
+
{
|
|
14
|
+
id: 'connectors',
|
|
15
|
+
title: 'Connectors',
|
|
16
|
+
description: 'Choose QQ, Weixin, Lingzhu, or another connector and configure it with arrows plus Enter.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'global-files',
|
|
20
|
+
title: 'Global Config Files',
|
|
21
|
+
description: 'Open raw runtime config files such as config.yaml, runners.yaml, and connectors.yaml.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'quest-files',
|
|
25
|
+
title: 'Current Quest Files',
|
|
26
|
+
description: 'Open quest-local config files for the currently selected quest.',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
9
29
|
const buildId = (prefix, raw) => `${prefix}:${raw}`;
|
|
10
30
|
const stringifyStructured = (value) => {
|
|
11
31
|
if (typeof value === 'string') {
|
|
@@ -107,6 +127,130 @@ const resolveConfigTarget = (token, items) => {
|
|
|
107
127
|
}
|
|
108
128
|
return (items.find((item) => item.name === trimmed || item.title === trimmed || item.configName === trimmed) ?? null);
|
|
109
129
|
};
|
|
130
|
+
const resolveManagedConnectorName = (token) => {
|
|
131
|
+
const normalized = String(token || '').trim().toLowerCase();
|
|
132
|
+
if (!normalized) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (CONNECTOR_ORDER.includes(normalized)) {
|
|
136
|
+
return normalized;
|
|
137
|
+
}
|
|
138
|
+
if (normalized === 'wechat') {
|
|
139
|
+
return 'weixin';
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
};
|
|
143
|
+
const connectorNamesFromStructuredConfig = (structured) => {
|
|
144
|
+
const known = new Set();
|
|
145
|
+
for (const name of CONNECTOR_ORDER) {
|
|
146
|
+
if (structured && typeof structured[name] === 'object') {
|
|
147
|
+
known.add(name);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return CONNECTOR_ORDER.filter((name) => known.has(name));
|
|
151
|
+
};
|
|
152
|
+
const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
153
|
+
const cloneStructured = (value) => {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(JSON.stringify(value));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return { ...value };
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const displayBoolean = (value) => (Boolean(value) ? 'on' : 'off');
|
|
162
|
+
const displayMaybeString = (value) => {
|
|
163
|
+
const text = String(value ?? '').trim();
|
|
164
|
+
return text || '—';
|
|
165
|
+
};
|
|
166
|
+
const CONNECTOR_BIND_ACTION_PREFIX = 'bind-target:';
|
|
167
|
+
const CONNECTOR_UNBIND_ACTION_PREFIX = 'unbind-target:';
|
|
168
|
+
const LINGZHU_PUBLIC_AGENT_ID = 'DeepScientist';
|
|
169
|
+
const LINGZHU_PLATFORM_AGENT_NAME = 'DeepScientist';
|
|
170
|
+
const LINGZHU_PLATFORM_CATEGORY = 'Work';
|
|
171
|
+
const LINGZHU_PLATFORM_INPUT_TYPE = 'Text';
|
|
172
|
+
const LINGZHU_PLATFORM_CAPABILITY_SUMMARY = 'DeepScientist is a local-first research agent for planning, experiments, analysis, writing, and execution follow-up.';
|
|
173
|
+
const LINGZHU_PLATFORM_OPENING_MESSAGE = 'Hello, I am DeepScientist. Tell me the research goal, experiment question, or task you want to move forward.';
|
|
174
|
+
const LINGZHU_PLATFORM_LOGO_PATH = '/assets/branding/logo-rokid.png';
|
|
175
|
+
const normalizedText = (value) => String(value ?? '').trim();
|
|
176
|
+
const normalizeBaseUrl = (value) => {
|
|
177
|
+
const text = normalizedText(value);
|
|
178
|
+
return text ? text.replace(/\/+$/, '') : '';
|
|
179
|
+
};
|
|
180
|
+
const looksLikePublicBaseUrl = (value) => {
|
|
181
|
+
const text = normalizeBaseUrl(value);
|
|
182
|
+
if (!text) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const url = new URL(text);
|
|
187
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const host = url.hostname.trim().toLowerCase();
|
|
191
|
+
if (!host || ['localhost', '0.0.0.0', '127.0.0.1', '::1'].includes(host) || host.endsWith('.local')) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (/^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[0-1])\./.test(host)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const lingzhuPublicSseUrl = (config, details) => {
|
|
204
|
+
const detailValue = normalizeBaseUrl(details.public_endpoint_url);
|
|
205
|
+
if (detailValue) {
|
|
206
|
+
return detailValue;
|
|
207
|
+
}
|
|
208
|
+
const base = normalizeBaseUrl(config.public_base_url);
|
|
209
|
+
return base ? `${base}/metis/agent/api/sse` : '';
|
|
210
|
+
};
|
|
211
|
+
const lingzhuPublicHealthUrl = (config, details) => {
|
|
212
|
+
const detailValue = normalizeBaseUrl(details.public_health_url);
|
|
213
|
+
if (detailValue) {
|
|
214
|
+
return detailValue;
|
|
215
|
+
}
|
|
216
|
+
const base = normalizeBaseUrl(config.public_base_url);
|
|
217
|
+
return base ? `${base}/metis/agent/api/health` : '';
|
|
218
|
+
};
|
|
219
|
+
const lingzhuPlatformLogoUrl = (baseUrl) => {
|
|
220
|
+
try {
|
|
221
|
+
const url = new URL(baseUrl);
|
|
222
|
+
if (url.hostname === '0.0.0.0') {
|
|
223
|
+
url.hostname = '127.0.0.1';
|
|
224
|
+
}
|
|
225
|
+
return new URL(LINGZHU_PLATFORM_LOGO_PATH, url.origin).toString();
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return LINGZHU_PLATFORM_LOGO_PATH;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const buildConnectorBindActionId = (connectorName, conversationId) => `${CONNECTOR_BIND_ACTION_PREFIX}${connectorName}:${encodeURIComponent(conversationId)}`;
|
|
232
|
+
const parseConnectorBindActionId = (actionId) => {
|
|
233
|
+
if (!actionId.startsWith(CONNECTOR_BIND_ACTION_PREFIX)) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const payload = actionId.slice(CONNECTOR_BIND_ACTION_PREFIX.length);
|
|
237
|
+
const separator = payload.indexOf(':');
|
|
238
|
+
if (separator < 0) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
connectorName: payload.slice(0, separator),
|
|
243
|
+
conversationId: decodeURIComponent(payload.slice(separator + 1)),
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
const buildConnectorUnbindActionId = (connectorName) => `${CONNECTOR_UNBIND_ACTION_PREFIX}${connectorName}`;
|
|
247
|
+
const parseConnectorUnbindActionId = (actionId) => {
|
|
248
|
+
if (!actionId.startsWith(CONNECTOR_UNBIND_ACTION_PREFIX)) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const connectorName = actionId.slice(CONNECTOR_UNBIND_ACTION_PREFIX.length).trim();
|
|
252
|
+
return connectorName ? connectorName : null;
|
|
253
|
+
};
|
|
110
254
|
function normalizeUpdate(raw) {
|
|
111
255
|
const eventType = String(raw.event_type ?? '');
|
|
112
256
|
const data = (raw.data ?? {});
|
|
@@ -363,10 +507,15 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
363
507
|
const [connectors, setConnectors] = useState([]);
|
|
364
508
|
const [activeQuestId, setActiveQuestId] = useState(initialQuestId);
|
|
365
509
|
const [browseQuestId, setBrowseQuestId] = useState(initialQuestId);
|
|
366
|
-
const [
|
|
510
|
+
const [configView, setConfigView] = useState(null);
|
|
367
511
|
const [configItems, setConfigItems] = useState([]);
|
|
368
512
|
const [configIndex, setConfigIndex] = useState(0);
|
|
513
|
+
const [configSectionTitle, setConfigSectionTitle] = useState('Config');
|
|
514
|
+
const [configSectionDescription, setConfigSectionDescription] = useState('');
|
|
369
515
|
const [configEditor, setConfigEditor] = useState(null);
|
|
516
|
+
const [connectorsDocument, setConnectorsDocument] = useState(null);
|
|
517
|
+
const [selectedConnectorName, setSelectedConnectorName] = useState(null);
|
|
518
|
+
const [weixinQrState, setWeixinQrState] = useState(null);
|
|
370
519
|
const [questPanelMode, setQuestPanelMode] = useState(null);
|
|
371
520
|
const [questPanelIndex, setQuestPanelIndex] = useState(0);
|
|
372
521
|
const [session, setSession] = useState(null);
|
|
@@ -387,7 +536,631 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
387
536
|
const activeQuest = useMemo(() => quests.find((quest) => quest.quest_id === activeQuestId) ?? null, [quests, activeQuestId]);
|
|
388
537
|
const browseQuest = useMemo(() => quests.find((quest) => quest.quest_id === browseQuestId) ?? null, [quests, browseQuestId]);
|
|
389
538
|
const panelQuests = useMemo(() => (questPanelMode ? getPanelQuests(questPanelMode, quests) : []), [questPanelMode, quests]);
|
|
539
|
+
const configMode = useMemo(() => (configView ? (configEditor ? 'edit' : 'browse') : null), [configEditor, configView]);
|
|
390
540
|
const selectedQuestForConfig = useMemo(() => activeQuest ?? browseQuest ?? null, [activeQuest, browseQuest]);
|
|
541
|
+
const selectedConnectorSnapshot = useMemo(() => selectedConnectorName
|
|
542
|
+
? connectors.find((item) => String(item.name || '').trim().toLowerCase() === selectedConnectorName) ?? null
|
|
543
|
+
: null, [connectors, selectedConnectorName]);
|
|
544
|
+
const selectedConnectorConfig = useMemo(() => (selectedConnectorName ? asRecord(connectorsDocument?.structured?.[selectedConnectorName]) : {}), [connectorsDocument?.structured, selectedConnectorName]);
|
|
545
|
+
const connectorMenuEntries = useMemo(() => {
|
|
546
|
+
const configuredNames = connectorNamesFromStructuredConfig(connectorsDocument?.structured ?? null);
|
|
547
|
+
const names = configuredNames.length > 0 ? configuredNames : CONNECTOR_ORDER;
|
|
548
|
+
return names.map((name) => {
|
|
549
|
+
const snapshot = connectors.find((item) => String(item.name || '').trim().toLowerCase() === name) ?? null;
|
|
550
|
+
return {
|
|
551
|
+
name,
|
|
552
|
+
label: connectorLabel(name),
|
|
553
|
+
subtitle: connectorSubtitle(name),
|
|
554
|
+
enabled: snapshot?.enabled !== false && Boolean(snapshot?.enabled || asRecord(connectorsDocument?.structured?.[name]).enabled),
|
|
555
|
+
connectionState: snapshot?.connection_state || snapshot?.auth_state || 'idle',
|
|
556
|
+
bindingCount: snapshot?.binding_count,
|
|
557
|
+
targetCount: snapshot?.target_count,
|
|
558
|
+
supportMode: supportsGuidedConnector(name) ? 'guided' : 'raw',
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
}, [connectors, connectorsDocument?.structured]);
|
|
562
|
+
const qqHasMultipleProfiles = useMemo(() => {
|
|
563
|
+
const profiles = selectedConnectorName === 'qq' ? selectedConnectorConfig.profiles : null;
|
|
564
|
+
return Array.isArray(profiles) && profiles.length > 1;
|
|
565
|
+
}, [selectedConnectorConfig, selectedConnectorName]);
|
|
566
|
+
const selectedConnectorTargets = useMemo(() => (selectedConnectorSnapshot ? normalizeConnectorTargets(selectedConnectorSnapshot) : []), [selectedConnectorSnapshot]);
|
|
567
|
+
const qqProfileSummaries = useMemo(() => {
|
|
568
|
+
if (selectedConnectorName !== 'qq') {
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
const configuredProfiles = Array.isArray(selectedConnectorConfig.profiles)
|
|
572
|
+
? selectedConnectorConfig.profiles
|
|
573
|
+
.filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item))
|
|
574
|
+
.map((item) => asRecord(item))
|
|
575
|
+
: [];
|
|
576
|
+
const runtimeProfiles = Array.isArray(selectedConnectorSnapshot?.profiles) ? selectedConnectorSnapshot.profiles : [];
|
|
577
|
+
const runtimeById = new Map(runtimeProfiles.map((profile) => [normalizedText(profile.profile_id), profile]));
|
|
578
|
+
const totalProfiles = configuredProfiles.length;
|
|
579
|
+
return configuredProfiles.map((profile, index) => {
|
|
580
|
+
const profileId = normalizedText(profile.profile_id || `qq-profile-${index + 1}`);
|
|
581
|
+
const runtime = runtimeById.get(profileId);
|
|
582
|
+
const profileTargets = selectedConnectorTargets.filter((target) => {
|
|
583
|
+
const targetProfileId = normalizedText(target.profile_id);
|
|
584
|
+
if (targetProfileId) {
|
|
585
|
+
return targetProfileId === profileId;
|
|
586
|
+
}
|
|
587
|
+
return totalProfiles === 1;
|
|
588
|
+
});
|
|
589
|
+
const mainChatId = normalizedText(profile.main_chat_id || runtime?.main_chat_id);
|
|
590
|
+
const selectedTarget = selectQqProfileTarget(profileTargets, mainChatId);
|
|
591
|
+
return {
|
|
592
|
+
profileId,
|
|
593
|
+
label: qqProfileDisplayLabel({
|
|
594
|
+
profile_id: profileId,
|
|
595
|
+
bot_name: normalizedText(profile.bot_name) || undefined,
|
|
596
|
+
app_id: normalizedText(profile.app_id) || undefined,
|
|
597
|
+
}, runtime ?? null),
|
|
598
|
+
appId: normalizedText(profile.app_id || runtime?.app_id),
|
|
599
|
+
mainChatId,
|
|
600
|
+
lastConversationId: normalizedText(runtime?.last_conversation_id),
|
|
601
|
+
targetCount: profileTargets.length,
|
|
602
|
+
selectedTarget,
|
|
603
|
+
status: qqProfileStatus(runtime ?? null, profileTargets, mainChatId),
|
|
604
|
+
};
|
|
605
|
+
});
|
|
606
|
+
}, [selectedConnectorConfig.profiles, selectedConnectorName, selectedConnectorSnapshot?.profiles, selectedConnectorTargets]);
|
|
607
|
+
const connectorContextLine = useMemo(() => {
|
|
608
|
+
if (selectedQuestForConfig?.quest_id) {
|
|
609
|
+
return `Current quest for binding: ${selectedQuestForConfig.quest_id}`;
|
|
610
|
+
}
|
|
611
|
+
return 'No active quest selected yet. You can still save connector settings.';
|
|
612
|
+
}, [selectedQuestForConfig?.quest_id]);
|
|
613
|
+
const connectorGuideSections = useMemo(() => {
|
|
614
|
+
if (!selectedConnectorName) {
|
|
615
|
+
return [];
|
|
616
|
+
}
|
|
617
|
+
if (selectedConnectorName === 'weixin') {
|
|
618
|
+
const accountId = normalizedText(selectedConnectorConfig.account_id || selectedConnectorSnapshot?.details?.account_id);
|
|
619
|
+
const loginUserId = normalizedText(selectedConnectorConfig.login_user_id);
|
|
620
|
+
const hasBinding = Boolean(accountId);
|
|
621
|
+
return [
|
|
622
|
+
{
|
|
623
|
+
id: 'weixin-flow',
|
|
624
|
+
title: 'Top Guide',
|
|
625
|
+
tone: hasBinding ? 'success' : 'info',
|
|
626
|
+
lines: [
|
|
627
|
+
'1. Choose Bind/Rebind Weixin to create a QR code inside TUI.',
|
|
628
|
+
'2. Scan the QR code with the WeChat app on the phone that owns this account, then confirm the login in WeChat.',
|
|
629
|
+
'3. DeepScientist saves the binding automatically and returns here after confirmation.',
|
|
630
|
+
],
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
id: 'weixin-status',
|
|
634
|
+
title: 'Current Status',
|
|
635
|
+
tone: hasBinding ? 'success' : 'warning',
|
|
636
|
+
lines: [
|
|
637
|
+
`Bot account: ${accountId || 'not bound yet'}`,
|
|
638
|
+
`Owner account: ${loginUserId || 'not bound yet'}`,
|
|
639
|
+
`Known targets: ${selectedConnectorTargets.length || selectedConnectorSnapshot?.target_count || 0}`,
|
|
640
|
+
],
|
|
641
|
+
},
|
|
642
|
+
];
|
|
643
|
+
}
|
|
644
|
+
if (selectedConnectorName === 'lingzhu') {
|
|
645
|
+
const details = asRecord(selectedConnectorSnapshot?.details);
|
|
646
|
+
const authAk = normalizedText(resolveLingzhuAuthAk(selectedConnectorConfig.auth_ak));
|
|
647
|
+
const publicBaseUrl = normalizeBaseUrl(selectedConnectorConfig.public_base_url);
|
|
648
|
+
const publicSseUrl = normalizedText(lingzhuPublicSseUrl(selectedConnectorConfig, details));
|
|
649
|
+
const publicReady = looksLikePublicBaseUrl(publicBaseUrl);
|
|
650
|
+
const tone = publicReady && authAk ? 'success' : 'warning';
|
|
651
|
+
return [
|
|
652
|
+
{
|
|
653
|
+
id: 'lingzhu-flow',
|
|
654
|
+
title: 'Top Guide',
|
|
655
|
+
tone,
|
|
656
|
+
lines: [
|
|
657
|
+
'1. Set Public base URL to the final public DeepScientist origin before binding a real Rokid device.',
|
|
658
|
+
'2. Copy the generated Rokid fields below into the platform exactly as shown, including Custom agent ID, URL, AK, Agent name, Category, Capability summary, Opening message, and Input type.',
|
|
659
|
+
'3. After the Rokid form is filled on the platform, return here and run Save Connector.',
|
|
660
|
+
],
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
id: 'lingzhu-platform',
|
|
664
|
+
title: 'Generated For Rokid',
|
|
665
|
+
tone,
|
|
666
|
+
lines: [
|
|
667
|
+
`Custom agent ID: ${normalizedText(selectedConnectorConfig.agent_id || LINGZHU_PUBLIC_AGENT_ID) || LINGZHU_PUBLIC_AGENT_ID}`,
|
|
668
|
+
`Custom agent URL: ${publicSseUrl || 'set Public base URL first'}`,
|
|
669
|
+
`Custom agent AK: ${authAk || 'generate or fill Custom agent AK first'}`,
|
|
670
|
+
`Agent name: ${LINGZHU_PLATFORM_AGENT_NAME}`,
|
|
671
|
+
`Category: ${LINGZHU_PLATFORM_CATEGORY}`,
|
|
672
|
+
`Capability summary: ${LINGZHU_PLATFORM_CAPABILITY_SUMMARY}`,
|
|
673
|
+
`Opening message: ${LINGZHU_PLATFORM_OPENING_MESSAGE}`,
|
|
674
|
+
`Input type: ${LINGZHU_PLATFORM_INPUT_TYPE}`,
|
|
675
|
+
],
|
|
676
|
+
},
|
|
677
|
+
...(publicReady
|
|
678
|
+
? []
|
|
679
|
+
: [
|
|
680
|
+
{
|
|
681
|
+
id: 'lingzhu-warning',
|
|
682
|
+
title: 'Public URL Required',
|
|
683
|
+
tone: 'warning',
|
|
684
|
+
lines: ['The current Public base URL is not a public HTTP(S) address yet, so Rokid devices will not be able to reach this bridge.'],
|
|
685
|
+
},
|
|
686
|
+
]),
|
|
687
|
+
];
|
|
688
|
+
}
|
|
689
|
+
if (selectedConnectorName === 'qq') {
|
|
690
|
+
const detectedOpenId = normalizedText(selectedConnectorConfig.main_chat_id || selectedConnectorSnapshot?.main_chat_id);
|
|
691
|
+
const lastConversationId = normalizedText(selectedConnectorSnapshot?.last_conversation_id);
|
|
692
|
+
const hasCredentials = Boolean(normalizedText(selectedConnectorConfig.app_id) && normalizedText(selectedConnectorConfig.app_secret));
|
|
693
|
+
const hasDetectedTarget = Boolean(detectedOpenId || selectedConnectorTargets.length);
|
|
694
|
+
const tone = hasDetectedTarget ? 'success' : hasCredentials ? 'warning' : 'info';
|
|
695
|
+
return [
|
|
696
|
+
{
|
|
697
|
+
id: 'qq-flow',
|
|
698
|
+
title: 'Top Guide',
|
|
699
|
+
tone,
|
|
700
|
+
lines: [
|
|
701
|
+
'1. Create the bot in the official QQ Bot Platform and copy App ID plus App Secret.',
|
|
702
|
+
qqHasMultipleProfiles
|
|
703
|
+
? '2. Multiple QQ profiles are configured. The TUI shows each profile status below, and you can still bind runtime targets here. Use raw connectors.yaml only for adding, deleting, or replacing profile credentials.'
|
|
704
|
+
: '2. Fill Bot name, App ID, and App Secret below, then run Save Connector.',
|
|
705
|
+
hasDetectedTarget
|
|
706
|
+
? '3. OpenID and conversation targets are already visible below. Bind the current quest to the correct target.'
|
|
707
|
+
: '3. After saving, send one private QQ message to the bot. TUI auto-refreshes Detected OpenID and conversation_id from runtime activity.',
|
|
708
|
+
],
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
id: 'qq-status',
|
|
712
|
+
title: 'Current Status',
|
|
713
|
+
tone,
|
|
714
|
+
lines: [
|
|
715
|
+
`Detected OpenID: ${detectedOpenId || 'waiting for first private message'}`,
|
|
716
|
+
`Last conversation: ${lastConversationId || 'waiting for runtime activity'}`,
|
|
717
|
+
`Configured profiles: ${qqProfileSummaries.length || 0}`,
|
|
718
|
+
`Discovered targets: ${selectedConnectorTargets.length}`,
|
|
719
|
+
],
|
|
720
|
+
},
|
|
721
|
+
...(!selectedQuestForConfig?.quest_id && hasDetectedTarget
|
|
722
|
+
? [
|
|
723
|
+
{
|
|
724
|
+
id: 'qq-bind-warning',
|
|
725
|
+
title: 'Quest Binding Needs A Quest',
|
|
726
|
+
tone: 'warning',
|
|
727
|
+
lines: ['Open a quest with `/use <quest_id>` first, then come back here to bind the detected QQ target to that quest.'],
|
|
728
|
+
},
|
|
729
|
+
]
|
|
730
|
+
: []),
|
|
731
|
+
];
|
|
732
|
+
}
|
|
733
|
+
return [];
|
|
734
|
+
}, [
|
|
735
|
+
qqProfileSummaries.length,
|
|
736
|
+
qqHasMultipleProfiles,
|
|
737
|
+
selectedConnectorConfig,
|
|
738
|
+
selectedConnectorName,
|
|
739
|
+
selectedConnectorSnapshot,
|
|
740
|
+
selectedConnectorTargets,
|
|
741
|
+
selectedQuestForConfig?.quest_id,
|
|
742
|
+
]);
|
|
743
|
+
const connectorDetailItems = useMemo(() => {
|
|
744
|
+
if (!selectedConnectorName) {
|
|
745
|
+
return [];
|
|
746
|
+
}
|
|
747
|
+
const activeBindingQuestId = selectedQuestForConfig?.quest_id || null;
|
|
748
|
+
const currentQuestTarget = activeBindingQuestId
|
|
749
|
+
? selectedConnectorTargets.find((item) => normalizedText(item.bound_quest_id) === activeBindingQuestId) || null
|
|
750
|
+
: null;
|
|
751
|
+
const bindingActionItems = [];
|
|
752
|
+
if (activeBindingQuestId && currentQuestTarget) {
|
|
753
|
+
bindingActionItems.push({
|
|
754
|
+
type: 'action',
|
|
755
|
+
id: buildConnectorUnbindActionId(selectedConnectorName),
|
|
756
|
+
label: `Unbind ${activeBindingQuestId}`,
|
|
757
|
+
description: `Remove ${activeBindingQuestId} from ${connectorLabel(selectedConnectorName)} and return the quest to local-only binding.`,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (activeBindingQuestId) {
|
|
761
|
+
for (const target of selectedConnectorTargets) {
|
|
762
|
+
const targetLabel = connectorTargetLabel(target) || target.conversation_id;
|
|
763
|
+
const bindingState = normalizedText(target.bound_quest_id) === activeBindingQuestId
|
|
764
|
+
? `Already bound to ${activeBindingQuestId}.`
|
|
765
|
+
: normalizedText(target.bound_quest_id)
|
|
766
|
+
? `Currently bound to ${target.bound_quest_id}. Enter will rebind it to ${activeBindingQuestId}.`
|
|
767
|
+
: `Not bound yet. Enter will bind it to ${activeBindingQuestId}.`;
|
|
768
|
+
bindingActionItems.push({
|
|
769
|
+
type: 'action',
|
|
770
|
+
id: buildConnectorBindActionId(selectedConnectorName, target.conversation_id),
|
|
771
|
+
label: `Bind ${targetLabel}`,
|
|
772
|
+
description: `${target.conversation_id} · ${bindingState}`,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (selectedConnectorName === 'weixin') {
|
|
777
|
+
return [
|
|
778
|
+
{
|
|
779
|
+
type: 'action',
|
|
780
|
+
id: 'weixin-start-login',
|
|
781
|
+
label: selectedConnectorSnapshot?.enabled ? 'Rebind Weixin' : 'Bind Weixin',
|
|
782
|
+
description: 'Start QR login, render a QR code in TUI, and save the connector automatically after confirmation.',
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
type: 'action',
|
|
786
|
+
id: 'refresh-connector',
|
|
787
|
+
label: 'Refresh Status',
|
|
788
|
+
description: 'Reload connector runtime snapshot and current structured config.',
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
type: 'info',
|
|
792
|
+
id: 'weixin-account-id',
|
|
793
|
+
label: 'Bot account',
|
|
794
|
+
value: displayMaybeString(selectedConnectorConfig.account_id ?? selectedConnectorSnapshot?.details?.account_id),
|
|
795
|
+
description: 'Saved Weixin bot account id.',
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
type: 'info',
|
|
799
|
+
id: 'weixin-login-user',
|
|
800
|
+
label: 'Owner account',
|
|
801
|
+
value: displayMaybeString(selectedConnectorConfig.login_user_id),
|
|
802
|
+
description: 'WeChat account that confirmed the QR login.',
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
type: 'info',
|
|
806
|
+
id: 'weixin-base-url',
|
|
807
|
+
label: 'Base URL',
|
|
808
|
+
value: displayMaybeString(selectedConnectorConfig.base_url),
|
|
809
|
+
description: 'The iLink API base URL saved in connectors config.',
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
type: 'info',
|
|
813
|
+
id: 'weixin-targets',
|
|
814
|
+
label: 'Known targets',
|
|
815
|
+
value: String(selectedConnectorTargets.length || selectedConnectorSnapshot?.target_count || 0),
|
|
816
|
+
description: 'Targets discovered by the Weixin connector.',
|
|
817
|
+
},
|
|
818
|
+
...bindingActionItems,
|
|
819
|
+
];
|
|
820
|
+
}
|
|
821
|
+
if (selectedConnectorName === 'lingzhu') {
|
|
822
|
+
const details = asRecord(selectedConnectorSnapshot?.details);
|
|
823
|
+
const authAk = resolveLingzhuAuthAk(selectedConnectorConfig.auth_ak);
|
|
824
|
+
const publicReady = looksLikePublicBaseUrl(selectedConnectorConfig.public_base_url);
|
|
825
|
+
const publicSseUrl = lingzhuPublicSseUrl(selectedConnectorConfig, details);
|
|
826
|
+
const publicHealthUrl = lingzhuPublicHealthUrl(selectedConnectorConfig, details);
|
|
827
|
+
const saveReady = publicReady && Boolean(normalizedText(publicSseUrl)) && Boolean(normalizedText(authAk));
|
|
828
|
+
const generatedAgentId = normalizedText(selectedConnectorConfig.agent_id || LINGZHU_PUBLIC_AGENT_ID) || LINGZHU_PUBLIC_AGENT_ID;
|
|
829
|
+
const logoUrl = lingzhuPlatformLogoUrl(baseUrl);
|
|
830
|
+
return [
|
|
831
|
+
{
|
|
832
|
+
type: 'action',
|
|
833
|
+
id: 'save-connector',
|
|
834
|
+
label: 'Save Connector',
|
|
835
|
+
description: saveReady
|
|
836
|
+
? 'Persist the generated Lingzhu values into connectors.yaml and reload the runtime.'
|
|
837
|
+
: 'Set a public base URL and a Custom agent AK first. The Web Rokid popup applies the same save gate.',
|
|
838
|
+
disabled: !saveReady,
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
type: 'action',
|
|
842
|
+
id: 'generate-lingzhu-ak',
|
|
843
|
+
label: 'Generate AK',
|
|
844
|
+
description: 'Create a new random Custom agent AK for the Lingzhu bridge.',
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
type: 'action',
|
|
848
|
+
id: 'refresh-connector',
|
|
849
|
+
label: 'Refresh Status',
|
|
850
|
+
description: 'Reload connector runtime snapshot and current structured config.',
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
type: 'field',
|
|
854
|
+
key: 'public_base_url',
|
|
855
|
+
label: 'Public base URL',
|
|
856
|
+
value: displayMaybeString(selectedConnectorConfig.public_base_url),
|
|
857
|
+
description: 'Publicly reachable DeepScientist base URL used by Rokid devices.',
|
|
858
|
+
fieldKind: 'url',
|
|
859
|
+
editable: true,
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
type: 'field',
|
|
863
|
+
key: 'local_host',
|
|
864
|
+
label: 'Local host',
|
|
865
|
+
value: displayMaybeString(selectedConnectorConfig.local_host),
|
|
866
|
+
description: 'Host used by DeepScientist when probing its own Lingzhu routes locally.',
|
|
867
|
+
fieldKind: 'text',
|
|
868
|
+
editable: true,
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
type: 'field',
|
|
872
|
+
key: 'gateway_port',
|
|
873
|
+
label: 'Gateway port',
|
|
874
|
+
value: displayMaybeString(selectedConnectorConfig.gateway_port),
|
|
875
|
+
description: 'Port used when building the health and SSE endpoints.',
|
|
876
|
+
fieldKind: 'text',
|
|
877
|
+
editable: true,
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
type: 'field',
|
|
881
|
+
key: 'agent_id',
|
|
882
|
+
label: 'Custom agent ID',
|
|
883
|
+
value: displayMaybeString(selectedConnectorConfig.agent_id || LINGZHU_PUBLIC_AGENT_ID),
|
|
884
|
+
description: 'Public agent id that must match the Rokid custom agent ID field.',
|
|
885
|
+
fieldKind: 'text',
|
|
886
|
+
editable: true,
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
type: 'field',
|
|
890
|
+
key: 'auth_ak',
|
|
891
|
+
label: 'Custom agent AK',
|
|
892
|
+
value: displayMaybeString(authAk),
|
|
893
|
+
description: 'Generated token that must match the AK pasted into the Rokid custom agent form.',
|
|
894
|
+
fieldKind: 'password',
|
|
895
|
+
editable: true,
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
type: 'info',
|
|
899
|
+
id: 'lingzhu-platform-agent-id',
|
|
900
|
+
label: 'Custom agent ID',
|
|
901
|
+
value: displayMaybeString(generatedAgentId),
|
|
902
|
+
description: 'Paste this exact value into the Rokid custom agent ID field.',
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
type: 'info',
|
|
906
|
+
id: 'lingzhu-platform-agent-url',
|
|
907
|
+
label: 'Custom agent URL',
|
|
908
|
+
value: displayMaybeString(publicSseUrl),
|
|
909
|
+
description: 'Paste this exact value into the Rokid custom agent URL field.',
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
type: 'info',
|
|
913
|
+
id: 'lingzhu-platform-agent-ak',
|
|
914
|
+
label: 'Custom agent AK',
|
|
915
|
+
value: displayMaybeString(authAk),
|
|
916
|
+
description: 'Paste this exact value into the Rokid custom agent AK field.',
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
type: 'info',
|
|
920
|
+
id: 'lingzhu-platform-agent-name',
|
|
921
|
+
label: 'Agent name',
|
|
922
|
+
value: LINGZHU_PLATFORM_AGENT_NAME,
|
|
923
|
+
description: 'Suggested display name for the Rokid custom agent form.',
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
type: 'info',
|
|
927
|
+
id: 'lingzhu-platform-category',
|
|
928
|
+
label: 'Category',
|
|
929
|
+
value: LINGZHU_PLATFORM_CATEGORY,
|
|
930
|
+
description: 'Recommended Rokid category for the DeepScientist agent.',
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
type: 'info',
|
|
934
|
+
id: 'lingzhu-platform-capability-summary',
|
|
935
|
+
label: 'Capability summary',
|
|
936
|
+
value: LINGZHU_PLATFORM_CAPABILITY_SUMMARY,
|
|
937
|
+
description: 'Paste this capability summary into the Rokid custom agent form.',
|
|
938
|
+
multiline: true,
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
type: 'info',
|
|
942
|
+
id: 'lingzhu-platform-opening-message',
|
|
943
|
+
label: 'Opening message',
|
|
944
|
+
value: LINGZHU_PLATFORM_OPENING_MESSAGE,
|
|
945
|
+
description: 'Paste this greeting into the Rokid opening message field.',
|
|
946
|
+
multiline: true,
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
type: 'info',
|
|
950
|
+
id: 'lingzhu-platform-input-type',
|
|
951
|
+
label: 'Input type',
|
|
952
|
+
value: LINGZHU_PLATFORM_INPUT_TYPE,
|
|
953
|
+
description: 'Recommended Rokid input type for DeepScientist.',
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
type: 'info',
|
|
957
|
+
id: 'lingzhu-platform-logo-url',
|
|
958
|
+
label: 'Icon/logo URL',
|
|
959
|
+
value: displayMaybeString(logoUrl),
|
|
960
|
+
description: 'Optional Rokid icon URL. The Web popup shows the same DeepScientist Rokid logo.',
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
type: 'info',
|
|
964
|
+
id: 'lingzhu-public-health',
|
|
965
|
+
label: 'Public health URL',
|
|
966
|
+
value: displayMaybeString(publicHealthUrl),
|
|
967
|
+
description: 'Public health endpoint for debugging.',
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
type: 'info',
|
|
971
|
+
id: 'lingzhu-local-sse',
|
|
972
|
+
label: 'Local SSE URL',
|
|
973
|
+
value: displayMaybeString(details.endpoint_url),
|
|
974
|
+
description: 'Local SSE endpoint used by DeepScientist probes.',
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
type: 'info',
|
|
978
|
+
id: 'lingzhu-local-health',
|
|
979
|
+
label: 'Local health URL',
|
|
980
|
+
value: displayMaybeString(details.health_url),
|
|
981
|
+
description: 'Local health endpoint used by DeepScientist probes.',
|
|
982
|
+
},
|
|
983
|
+
...bindingActionItems,
|
|
984
|
+
];
|
|
985
|
+
}
|
|
986
|
+
if (selectedConnectorName === 'qq') {
|
|
987
|
+
const secretValue = String(selectedConnectorConfig.app_secret ?? '').trim();
|
|
988
|
+
const qqProfileInfoItems = qqProfileSummaries.map((profile) => {
|
|
989
|
+
const statusText = profile.status === 'bound'
|
|
990
|
+
? 'Bound'
|
|
991
|
+
: profile.status === 'ready'
|
|
992
|
+
? 'Ready for binding'
|
|
993
|
+
: 'Waiting for first message';
|
|
994
|
+
const targetLabel = profile.selectedTarget ? connectorTargetLabel(profile.selectedTarget) || profile.selectedTarget.conversation_id : 'waiting';
|
|
995
|
+
const boundQuestId = normalizedText(profile.selectedTarget?.bound_quest_id) || 'not bound yet';
|
|
996
|
+
return {
|
|
997
|
+
type: 'info',
|
|
998
|
+
id: `qq-profile:${profile.profileId}`,
|
|
999
|
+
label: `Profile · ${profile.label}`,
|
|
1000
|
+
value: [
|
|
1001
|
+
`Profile ID: ${profile.profileId}`,
|
|
1002
|
+
`App ID: ${profile.appId || '—'}`,
|
|
1003
|
+
`Detected OpenID: ${profile.mainChatId || 'waiting for first private message'}`,
|
|
1004
|
+
`Last conversation: ${profile.lastConversationId || 'waiting for runtime activity'}`,
|
|
1005
|
+
`Targets: ${profile.targetCount}`,
|
|
1006
|
+
`Preferred target: ${targetLabel}`,
|
|
1007
|
+
`Bound quest: ${boundQuestId}`,
|
|
1008
|
+
`Status: ${statusText}`,
|
|
1009
|
+
].join('\n'),
|
|
1010
|
+
description: 'Profile-level runtime summary aligned with the Web QQ connector cards.',
|
|
1011
|
+
multiline: true,
|
|
1012
|
+
};
|
|
1013
|
+
});
|
|
1014
|
+
return [
|
|
1015
|
+
{
|
|
1016
|
+
type: 'action',
|
|
1017
|
+
id: 'save-connector',
|
|
1018
|
+
label: 'Save Connector',
|
|
1019
|
+
description: qqHasMultipleProfiles
|
|
1020
|
+
? 'Persist shared QQ settings and reload the runtime. Use raw connectors.yaml for profile add, delete, or credential replacement.'
|
|
1021
|
+
: 'Persist the current QQ fields into connectors.yaml and reload the runtime.',
|
|
1022
|
+
disabled: false,
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
type: 'action',
|
|
1026
|
+
id: 'refresh-connector',
|
|
1027
|
+
label: 'Refresh Status',
|
|
1028
|
+
description: 'Reload connector runtime snapshot and current structured config.',
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
type: 'action',
|
|
1032
|
+
id: 'open-raw-connectors',
|
|
1033
|
+
label: 'Open Raw connectors.yaml',
|
|
1034
|
+
description: 'Open the full connectors config file for advanced or multi-profile QQ changes.',
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
type: 'field',
|
|
1038
|
+
key: 'bot_name',
|
|
1039
|
+
label: 'Bot name',
|
|
1040
|
+
value: displayMaybeString(selectedConnectorConfig.bot_name),
|
|
1041
|
+
description: qqHasMultipleProfiles
|
|
1042
|
+
? 'Default display name for QQ profiles. Add or replace per-profile bot names in raw connectors.yaml.'
|
|
1043
|
+
: 'Display name used by the QQ connector.',
|
|
1044
|
+
fieldKind: 'text',
|
|
1045
|
+
editable: !qqHasMultipleProfiles,
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
type: 'field',
|
|
1049
|
+
key: 'app_id',
|
|
1050
|
+
label: 'App ID',
|
|
1051
|
+
value: displayMaybeString(selectedConnectorConfig.app_id),
|
|
1052
|
+
description: 'Tencent QQ bot App ID.',
|
|
1053
|
+
fieldKind: 'text',
|
|
1054
|
+
editable: !qqHasMultipleProfiles,
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
type: 'field',
|
|
1058
|
+
key: 'app_secret',
|
|
1059
|
+
label: 'App secret',
|
|
1060
|
+
value: displayMaybeString(secretValue),
|
|
1061
|
+
description: 'QQ bot App Secret. Save this first, then send one private QQ message to the bot.',
|
|
1062
|
+
fieldKind: 'password',
|
|
1063
|
+
editable: !qqHasMultipleProfiles,
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
type: 'field',
|
|
1067
|
+
key: 'command_prefix',
|
|
1068
|
+
label: 'Command prefix',
|
|
1069
|
+
value: displayMaybeString(selectedConnectorConfig.command_prefix),
|
|
1070
|
+
description: 'Prefix used for slash-style QQ commands.',
|
|
1071
|
+
fieldKind: 'text',
|
|
1072
|
+
editable: true,
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
type: 'field',
|
|
1076
|
+
key: 'require_at_in_groups',
|
|
1077
|
+
label: 'Require @ mention in groups',
|
|
1078
|
+
value: displayBoolean(selectedConnectorConfig.require_at_in_groups),
|
|
1079
|
+
description: 'Only process QQ group messages when the bot is explicitly mentioned.',
|
|
1080
|
+
fieldKind: 'boolean',
|
|
1081
|
+
editable: true,
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
type: 'field',
|
|
1085
|
+
key: 'gateway_restart_on_config_change',
|
|
1086
|
+
label: 'Restart gateway on config change',
|
|
1087
|
+
value: displayBoolean(selectedConnectorConfig.gateway_restart_on_config_change),
|
|
1088
|
+
description: 'Restart the local gateway worker after QQ settings are changed.',
|
|
1089
|
+
fieldKind: 'boolean',
|
|
1090
|
+
editable: true,
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
type: 'field',
|
|
1094
|
+
key: 'auto_bind_dm_to_active_quest',
|
|
1095
|
+
label: 'Auto-bind DM to active quest',
|
|
1096
|
+
value: displayBoolean(selectedConnectorConfig.auto_bind_dm_to_active_quest),
|
|
1097
|
+
description: 'Allow a private QQ chat to auto-bind to the current quest.',
|
|
1098
|
+
fieldKind: 'boolean',
|
|
1099
|
+
editable: true,
|
|
1100
|
+
},
|
|
1101
|
+
...qqProfileInfoItems,
|
|
1102
|
+
{
|
|
1103
|
+
type: 'info',
|
|
1104
|
+
id: 'qq-detected-openid',
|
|
1105
|
+
label: 'Detected OpenID',
|
|
1106
|
+
value: displayMaybeString(selectedConnectorConfig.main_chat_id || selectedConnectorSnapshot?.main_chat_id),
|
|
1107
|
+
description: 'This value is auto-filled after the first private QQ message reaches DeepScientist.',
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
type: 'info',
|
|
1111
|
+
id: 'qq-last-conversation',
|
|
1112
|
+
label: 'Last conversation',
|
|
1113
|
+
value: displayMaybeString(selectedConnectorSnapshot?.last_conversation_id),
|
|
1114
|
+
description: 'After the first QQ message arrives, the runtime also refreshes the latest conversation id here.',
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
type: 'info',
|
|
1118
|
+
id: 'qq-targets',
|
|
1119
|
+
label: 'Discovered targets',
|
|
1120
|
+
value: String(selectedConnectorTargets.length || selectedConnectorSnapshot?.target_count || 0),
|
|
1121
|
+
description: 'Targets learned from QQ runtime activity. Bind the current quest to one of them below.',
|
|
1122
|
+
},
|
|
1123
|
+
...bindingActionItems,
|
|
1124
|
+
];
|
|
1125
|
+
}
|
|
1126
|
+
return [
|
|
1127
|
+
{
|
|
1128
|
+
type: 'action',
|
|
1129
|
+
id: 'open-raw-connectors',
|
|
1130
|
+
label: 'Open Raw connectors.yaml',
|
|
1131
|
+
description: 'Guided setup is not available for this connector yet. Open the raw connectors config instead.',
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
type: 'info',
|
|
1135
|
+
id: 'connector-mode',
|
|
1136
|
+
label: 'Mode',
|
|
1137
|
+
value: displayMaybeString(selectedConnectorSnapshot?.mode || selectedConnectorConfig.transport),
|
|
1138
|
+
description: 'Current connector runtime mode.',
|
|
1139
|
+
},
|
|
1140
|
+
...bindingActionItems,
|
|
1141
|
+
];
|
|
1142
|
+
}, [
|
|
1143
|
+
baseUrl,
|
|
1144
|
+
qqProfileSummaries,
|
|
1145
|
+
qqHasMultipleProfiles,
|
|
1146
|
+
selectedConnectorConfig,
|
|
1147
|
+
selectedConnectorName,
|
|
1148
|
+
selectedConnectorSnapshot,
|
|
1149
|
+
selectedConnectorTargets,
|
|
1150
|
+
selectedQuestForConfig?.quest_id,
|
|
1151
|
+
]);
|
|
1152
|
+
const connectorDirty = useMemo(() => {
|
|
1153
|
+
if (!selectedConnectorName || !connectorsDocument?.structured) {
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
try {
|
|
1157
|
+
return (JSON.stringify(asRecord(connectorsDocument.structured[selectedConnectorName])) !==
|
|
1158
|
+
JSON.stringify(asRecord(connectorsDocument.savedStructured[selectedConnectorName])));
|
|
1159
|
+
}
|
|
1160
|
+
catch {
|
|
1161
|
+
return true;
|
|
1162
|
+
}
|
|
1163
|
+
}, [connectorsDocument, selectedConnectorName]);
|
|
391
1164
|
const slashSuggestions = useMemo(() => {
|
|
392
1165
|
const slashCommands = session?.acp_session?.slash_commands ?? [];
|
|
393
1166
|
const localCommands = [
|
|
@@ -401,8 +1174,11 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
401
1174
|
{ name: '/stop', description: 'Stop a running quest.' },
|
|
402
1175
|
{ name: '/status', description: 'Show the current quest status.' },
|
|
403
1176
|
{ name: '/graph', description: 'Show the quest graph.' },
|
|
404
|
-
{ name: '/config', description: 'Open the local config
|
|
405
|
-
{ name: '/config connectors', description: 'Open
|
|
1177
|
+
{ name: '/config', description: 'Open the local config workspace.' },
|
|
1178
|
+
{ name: '/config connectors', description: 'Open the connector list inside config.' },
|
|
1179
|
+
{ name: '/config qq', description: 'Open the QQ connector setup.' },
|
|
1180
|
+
{ name: '/config weixin', description: 'Open the Weixin connector setup.' },
|
|
1181
|
+
{ name: '/config lingzhu', description: 'Open the Lingzhu connector setup.' },
|
|
406
1182
|
];
|
|
407
1183
|
if (!input.startsWith('/')) {
|
|
408
1184
|
return [];
|
|
@@ -418,6 +1194,111 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
418
1194
|
session?.acp_session?.meta?.default_reply_interaction_id;
|
|
419
1195
|
return snapshotTarget ? String(snapshotTarget) : null;
|
|
420
1196
|
}, [session]);
|
|
1197
|
+
const configPanel = useMemo(() => {
|
|
1198
|
+
if (!configView) {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
if (configEditor?.kind === 'document') {
|
|
1202
|
+
return {
|
|
1203
|
+
kind: 'document-editor',
|
|
1204
|
+
item: configEditor.item,
|
|
1205
|
+
content: input,
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
if (configEditor?.kind === 'connector-field') {
|
|
1209
|
+
return {
|
|
1210
|
+
kind: 'connector-field-editor',
|
|
1211
|
+
connectorName: connectorLabel(configEditor.connectorName),
|
|
1212
|
+
fieldLabel: configEditor.fieldLabel,
|
|
1213
|
+
content: input,
|
|
1214
|
+
description: configEditor.description,
|
|
1215
|
+
masked: configEditor.fieldKind === 'password',
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
if (configView === 'root') {
|
|
1219
|
+
return {
|
|
1220
|
+
kind: 'root',
|
|
1221
|
+
items: CONFIG_ROOT_ENTRIES,
|
|
1222
|
+
selectedIndex: configIndex,
|
|
1223
|
+
selectedQuestId: selectedQuestForConfig?.quest_id ?? null,
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
if (configView === 'files') {
|
|
1227
|
+
return {
|
|
1228
|
+
kind: 'files',
|
|
1229
|
+
title: configSectionTitle,
|
|
1230
|
+
description: configSectionDescription,
|
|
1231
|
+
items: configItems,
|
|
1232
|
+
selectedIndex: configIndex,
|
|
1233
|
+
selectedQuestId: selectedQuestForConfig?.quest_id ?? null,
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
if (configView === 'connector-list') {
|
|
1237
|
+
return {
|
|
1238
|
+
kind: 'connector-list',
|
|
1239
|
+
items: connectorMenuEntries,
|
|
1240
|
+
selectedIndex: configIndex,
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
if (configView === 'connector-detail' && selectedConnectorName) {
|
|
1244
|
+
return {
|
|
1245
|
+
kind: 'connector-detail',
|
|
1246
|
+
connectorName: selectedConnectorName,
|
|
1247
|
+
connectorLabel: connectorLabel(selectedConnectorName),
|
|
1248
|
+
selectedIndex: configIndex,
|
|
1249
|
+
items: connectorDetailItems,
|
|
1250
|
+
dirty: connectorDirty,
|
|
1251
|
+
snapshot: selectedConnectorSnapshot,
|
|
1252
|
+
contextLine: connectorContextLine,
|
|
1253
|
+
guideSections: connectorGuideSections,
|
|
1254
|
+
warning: selectedConnectorName === 'qq' && qqHasMultipleProfiles
|
|
1255
|
+
? 'QQ currently has multiple profiles. TUI supports shared settings and quest binding here, but adding, deleting, or replacing profile credentials still belongs in raw connectors.yaml.'
|
|
1256
|
+
: null,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
if (configView === 'weixin-qr') {
|
|
1260
|
+
return {
|
|
1261
|
+
kind: 'weixin-qr',
|
|
1262
|
+
status: weixinQrState?.status || 'waiting',
|
|
1263
|
+
sessionKey: weixinQrState?.sessionKey || null,
|
|
1264
|
+
qrAscii: weixinQrState?.qrAscii || null,
|
|
1265
|
+
qrContent: weixinQrState?.qrContent || null,
|
|
1266
|
+
qrUrl: weixinQrState?.qrUrl || null,
|
|
1267
|
+
message: weixinQrState?.message || null,
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
return null;
|
|
1271
|
+
}, [
|
|
1272
|
+
configEditor,
|
|
1273
|
+
configIndex,
|
|
1274
|
+
configItems,
|
|
1275
|
+
configSectionDescription,
|
|
1276
|
+
configSectionTitle,
|
|
1277
|
+
connectorContextLine,
|
|
1278
|
+
configView,
|
|
1279
|
+
connectorDetailItems,
|
|
1280
|
+
connectorDirty,
|
|
1281
|
+
connectorGuideSections,
|
|
1282
|
+
connectorMenuEntries,
|
|
1283
|
+
input,
|
|
1284
|
+
qqHasMultipleProfiles,
|
|
1285
|
+
selectedConnectorName,
|
|
1286
|
+
selectedConnectorSnapshot,
|
|
1287
|
+
selectedQuestForConfig?.quest_id,
|
|
1288
|
+
weixinQrState,
|
|
1289
|
+
]);
|
|
1290
|
+
const configSelectionCount = useMemo(() => {
|
|
1291
|
+
if (!configPanel || configMode !== 'browse') {
|
|
1292
|
+
return 0;
|
|
1293
|
+
}
|
|
1294
|
+
if (configPanel.kind === 'root' || configPanel.kind === 'files' || configPanel.kind === 'connector-list') {
|
|
1295
|
+
return configPanel.items.length;
|
|
1296
|
+
}
|
|
1297
|
+
if (configPanel.kind === 'connector-detail') {
|
|
1298
|
+
return configPanel.items.length;
|
|
1299
|
+
}
|
|
1300
|
+
return 0;
|
|
1301
|
+
}, [configMode, configPanel]);
|
|
421
1302
|
useEffect(() => {
|
|
422
1303
|
activeQuestIdRef.current = activeQuestId;
|
|
423
1304
|
}, [activeQuestId]);
|
|
@@ -437,10 +1318,15 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
437
1318
|
browseQuestIdRef.current = questId;
|
|
438
1319
|
setBrowseQuestId(questId);
|
|
439
1320
|
}
|
|
440
|
-
|
|
1321
|
+
setConfigView(null);
|
|
441
1322
|
setConfigEditor(null);
|
|
442
1323
|
setConfigItems([]);
|
|
1324
|
+
setConfigSectionTitle('Config');
|
|
1325
|
+
setConfigSectionDescription('');
|
|
443
1326
|
setConfigIndex(0);
|
|
1327
|
+
setConnectorsDocument(null);
|
|
1328
|
+
setSelectedConnectorName(null);
|
|
1329
|
+
setWeixinQrState(null);
|
|
444
1330
|
setQuestPanelMode(null);
|
|
445
1331
|
historyRef.current = [];
|
|
446
1332
|
pendingHistoryItemsRef.current = [];
|
|
@@ -454,10 +1340,15 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
454
1340
|
activeQuestIdRef.current = null;
|
|
455
1341
|
browseQuestIdRef.current = null;
|
|
456
1342
|
setActiveQuestId(null);
|
|
457
|
-
|
|
1343
|
+
setConfigView(null);
|
|
458
1344
|
setConfigEditor(null);
|
|
459
1345
|
setConfigItems([]);
|
|
1346
|
+
setConfigSectionTitle('Config');
|
|
1347
|
+
setConfigSectionDescription('');
|
|
460
1348
|
setConfigIndex(0);
|
|
1349
|
+
setConnectorsDocument(null);
|
|
1350
|
+
setSelectedConnectorName(null);
|
|
1351
|
+
setWeixinQrState(null);
|
|
461
1352
|
setQuestPanelMode(null);
|
|
462
1353
|
setSession(null);
|
|
463
1354
|
historyRef.current = [];
|
|
@@ -553,8 +1444,11 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
553
1444
|
}
|
|
554
1445
|
}, [baseUrl]);
|
|
555
1446
|
const openQuestPanel = useCallback((mode) => {
|
|
556
|
-
|
|
1447
|
+
setConfigView(null);
|
|
557
1448
|
setConfigEditor(null);
|
|
1449
|
+
setConnectorsDocument(null);
|
|
1450
|
+
setSelectedConnectorName(null);
|
|
1451
|
+
setWeixinQrState(null);
|
|
558
1452
|
const candidates = getPanelQuests(mode, quests);
|
|
559
1453
|
setQuestPanelMode(mode);
|
|
560
1454
|
if (candidates.length === 0) {
|
|
@@ -581,15 +1475,60 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
581
1475
|
: 'Resume quest · use arrows and Enter to resume.');
|
|
582
1476
|
}, [activeQuestId, browseQuestId, quests]);
|
|
583
1477
|
const closeConfigScreen = useCallback((nextStatus) => {
|
|
584
|
-
|
|
1478
|
+
setConfigView(null);
|
|
585
1479
|
setConfigEditor(null);
|
|
586
1480
|
setConfigItems([]);
|
|
1481
|
+
setConfigSectionTitle('Config');
|
|
1482
|
+
setConfigSectionDescription('');
|
|
587
1483
|
setConfigIndex(0);
|
|
1484
|
+
setConnectorsDocument(null);
|
|
1485
|
+
setSelectedConnectorName(null);
|
|
1486
|
+
setWeixinQrState(null);
|
|
588
1487
|
setInput('');
|
|
589
1488
|
if (nextStatus) {
|
|
590
1489
|
setStatusLine(nextStatus);
|
|
591
1490
|
}
|
|
592
1491
|
}, []);
|
|
1492
|
+
const loadConnectorsDocument = useCallback(async () => {
|
|
1493
|
+
const payload = await client.configDocument(baseUrl, 'connectors');
|
|
1494
|
+
const structured = asRecord(payload.meta?.structured_config);
|
|
1495
|
+
return {
|
|
1496
|
+
item: {
|
|
1497
|
+
id: 'global:connectors',
|
|
1498
|
+
scope: 'global',
|
|
1499
|
+
name: 'connectors',
|
|
1500
|
+
title: payload.title || 'connectors.yaml',
|
|
1501
|
+
path: payload.path,
|
|
1502
|
+
writable: true,
|
|
1503
|
+
configName: 'connectors',
|
|
1504
|
+
},
|
|
1505
|
+
revision: payload.revision,
|
|
1506
|
+
savedStructured: cloneStructured(structured),
|
|
1507
|
+
structured: cloneStructured(structured),
|
|
1508
|
+
};
|
|
1509
|
+
}, [baseUrl]);
|
|
1510
|
+
const setWeixinQrPayload = useCallback(async (payload) => {
|
|
1511
|
+
const qrContent = String(payload.qrcode_content || '').trim();
|
|
1512
|
+
const qrUrl = String(payload.qrcode_url || '').trim();
|
|
1513
|
+
let qrAscii = '';
|
|
1514
|
+
const renderable = qrContent || qrUrl;
|
|
1515
|
+
if (renderable && !looksLikeWeixinQrImageUrl(qrUrl || qrContent)) {
|
|
1516
|
+
try {
|
|
1517
|
+
qrAscii = await renderQrAscii(qrContent || qrUrl);
|
|
1518
|
+
}
|
|
1519
|
+
catch {
|
|
1520
|
+
qrAscii = '';
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
setWeixinQrState({
|
|
1524
|
+
sessionKey: String(payload.session_key || '').trim(),
|
|
1525
|
+
status: String(payload.status || 'wait').trim() || 'wait',
|
|
1526
|
+
qrContent: qrContent || undefined,
|
|
1527
|
+
qrUrl: qrUrl || undefined,
|
|
1528
|
+
qrAscii: qrAscii || undefined,
|
|
1529
|
+
message: String(payload.message || '').trim() || undefined,
|
|
1530
|
+
});
|
|
1531
|
+
}, []);
|
|
593
1532
|
const openConfigEditor = useCallback(async (item) => {
|
|
594
1533
|
try {
|
|
595
1534
|
let payload;
|
|
@@ -605,8 +1544,9 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
605
1544
|
payload = await client.openDocument(baseUrl, questId, item.documentId);
|
|
606
1545
|
}
|
|
607
1546
|
setQuestPanelMode(null);
|
|
608
|
-
|
|
1547
|
+
setConfigView('files');
|
|
609
1548
|
setConfigEditor({
|
|
1549
|
+
kind: 'document',
|
|
610
1550
|
item,
|
|
611
1551
|
revision: payload.revision,
|
|
612
1552
|
content: payload.content,
|
|
@@ -618,19 +1558,22 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
618
1558
|
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
619
1559
|
}
|
|
620
1560
|
}, [baseUrl, selectedQuestForConfig]);
|
|
621
|
-
const
|
|
1561
|
+
const openConfigFiles = useCallback(async (scope, target) => {
|
|
622
1562
|
try {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const nextItems = [...globalItems, ...questItems];
|
|
1563
|
+
const nextItems = scope === 'global'
|
|
1564
|
+
? buildGlobalConfigItems(await client.configFiles(baseUrl))
|
|
1565
|
+
: buildQuestConfigItems(selectedQuestForConfig?.quest_id ?? null, selectedQuestForConfig?.quest_root);
|
|
627
1566
|
setQuestPanelMode(null);
|
|
1567
|
+
setConfigView('files');
|
|
628
1568
|
setConfigItems(nextItems);
|
|
1569
|
+
setConfigSectionTitle(scope === 'global' ? 'Global Config Files' : 'Current Quest Files');
|
|
1570
|
+
setConfigSectionDescription(scope === 'global'
|
|
1571
|
+
? 'Choose a global config file and press Enter to edit it.'
|
|
1572
|
+
: 'Choose a quest-local config file and press Enter to edit it.');
|
|
629
1573
|
setConfigEditor(null);
|
|
630
|
-
setConfigMode('browse');
|
|
631
1574
|
if (nextItems.length === 0) {
|
|
632
1575
|
setConfigIndex(0);
|
|
633
|
-
setStatusLine('No config files available.');
|
|
1576
|
+
setStatusLine(scope === 'global' ? 'No global config files available.' : 'No current quest config files available.');
|
|
634
1577
|
return;
|
|
635
1578
|
}
|
|
636
1579
|
if (target) {
|
|
@@ -642,17 +1585,174 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
642
1585
|
}
|
|
643
1586
|
}
|
|
644
1587
|
setConfigIndex(0);
|
|
645
|
-
setStatusLine('
|
|
1588
|
+
setStatusLine(scope === 'global' ? 'Global config files.' : 'Current quest config files.');
|
|
646
1589
|
}
|
|
647
1590
|
catch (error) {
|
|
648
1591
|
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
649
1592
|
}
|
|
650
1593
|
}, [baseUrl, openConfigEditor, selectedQuestForConfig]);
|
|
1594
|
+
const openConfigRoot = useCallback((nextStatus = 'Config · choose a section with arrows and Enter.') => {
|
|
1595
|
+
setQuestPanelMode(null);
|
|
1596
|
+
setConfigView('root');
|
|
1597
|
+
setConfigEditor(null);
|
|
1598
|
+
setConfigItems([]);
|
|
1599
|
+
setConfigSectionTitle('Config');
|
|
1600
|
+
setConfigSectionDescription('');
|
|
1601
|
+
setConfigIndex(0);
|
|
1602
|
+
setSelectedConnectorName(null);
|
|
1603
|
+
setWeixinQrState(null);
|
|
1604
|
+
setInput('');
|
|
1605
|
+
setStatusLine(nextStatus);
|
|
1606
|
+
}, []);
|
|
1607
|
+
const openConnectorBrowser = useCallback(async (targetConnector) => {
|
|
1608
|
+
try {
|
|
1609
|
+
const document = await loadConnectorsDocument();
|
|
1610
|
+
const names = connectorNamesFromStructuredConfig(document.structured);
|
|
1611
|
+
const browseNames = names.length > 0 ? names : CONNECTOR_ORDER;
|
|
1612
|
+
setQuestPanelMode(null);
|
|
1613
|
+
setConfigView('connector-list');
|
|
1614
|
+
setConfigEditor(null);
|
|
1615
|
+
setConnectorsDocument(document);
|
|
1616
|
+
setSelectedConnectorName(null);
|
|
1617
|
+
setWeixinQrState(null);
|
|
1618
|
+
setInput('');
|
|
1619
|
+
if (targetConnector) {
|
|
1620
|
+
const directIndex = browseNames.findIndex((item) => item === targetConnector);
|
|
1621
|
+
setConfigIndex(directIndex >= 0 ? directIndex : 0);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
setConfigIndex(0);
|
|
1625
|
+
setStatusLine('Connectors · choose a connector with arrows and Enter.');
|
|
1626
|
+
}
|
|
1627
|
+
catch (error) {
|
|
1628
|
+
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
1629
|
+
}
|
|
1630
|
+
}, [loadConnectorsDocument]);
|
|
1631
|
+
const updateConnectorDraft = useCallback((connectorName, patch) => {
|
|
1632
|
+
setConnectorsDocument((current) => {
|
|
1633
|
+
if (!current) {
|
|
1634
|
+
return current;
|
|
1635
|
+
}
|
|
1636
|
+
const nextStructured = cloneStructured(current.structured);
|
|
1637
|
+
const currentConnector = asRecord(nextStructured[connectorName]);
|
|
1638
|
+
nextStructured[connectorName] = {
|
|
1639
|
+
...currentConnector,
|
|
1640
|
+
...patch,
|
|
1641
|
+
};
|
|
1642
|
+
return {
|
|
1643
|
+
...current,
|
|
1644
|
+
structured: nextStructured,
|
|
1645
|
+
};
|
|
1646
|
+
});
|
|
1647
|
+
}, []);
|
|
1648
|
+
const openConnectorDetail = useCallback(async (connectorName) => {
|
|
1649
|
+
try {
|
|
1650
|
+
let currentDocument = connectorsDocument ?? (await loadConnectorsDocument());
|
|
1651
|
+
if (connectorName === 'lingzhu') {
|
|
1652
|
+
const currentLingzhu = asRecord(currentDocument.structured.lingzhu);
|
|
1653
|
+
const patch = {};
|
|
1654
|
+
if (!resolveLingzhuAuthAk(currentLingzhu.auth_ak)) {
|
|
1655
|
+
patch.auth_ak = createLingzhuAk();
|
|
1656
|
+
}
|
|
1657
|
+
if (!String(currentLingzhu.agent_id || '').trim()) {
|
|
1658
|
+
patch.agent_id = LINGZHU_PUBLIC_AGENT_ID;
|
|
1659
|
+
}
|
|
1660
|
+
if (!String(currentLingzhu.local_host || '').trim()) {
|
|
1661
|
+
patch.local_host = '127.0.0.1';
|
|
1662
|
+
}
|
|
1663
|
+
if (!String(currentLingzhu.gateway_port || '').trim()) {
|
|
1664
|
+
try {
|
|
1665
|
+
patch.gateway_port = String(new URL(baseUrl).port || '20999');
|
|
1666
|
+
}
|
|
1667
|
+
catch {
|
|
1668
|
+
patch.gateway_port = '20999';
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
if (!String(currentLingzhu.public_base_url || '').trim()) {
|
|
1672
|
+
try {
|
|
1673
|
+
const currentBaseUrl = new URL(baseUrl);
|
|
1674
|
+
if (currentBaseUrl.hostname === '0.0.0.0') {
|
|
1675
|
+
currentBaseUrl.hostname = '127.0.0.1';
|
|
1676
|
+
}
|
|
1677
|
+
const resolvedBaseUrl = currentBaseUrl.toString().replace(/\/$/, '');
|
|
1678
|
+
if (looksLikePublicBaseUrl(resolvedBaseUrl)) {
|
|
1679
|
+
patch.public_base_url = resolvedBaseUrl;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
catch {
|
|
1683
|
+
// ignore parse failures
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
if (Object.keys(patch).length > 0) {
|
|
1687
|
+
currentDocument = {
|
|
1688
|
+
...currentDocument,
|
|
1689
|
+
structured: {
|
|
1690
|
+
...cloneStructured(currentDocument.structured),
|
|
1691
|
+
lingzhu: {
|
|
1692
|
+
...currentLingzhu,
|
|
1693
|
+
...patch,
|
|
1694
|
+
},
|
|
1695
|
+
},
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
setConnectorsDocument(currentDocument);
|
|
1700
|
+
setSelectedConnectorName(connectorName);
|
|
1701
|
+
setConfigView('connector-detail');
|
|
1702
|
+
setConfigEditor(null);
|
|
1703
|
+
setWeixinQrState(null);
|
|
1704
|
+
setConfigIndex(0);
|
|
1705
|
+
setInput('');
|
|
1706
|
+
setStatusLine(`${connectorLabel(connectorName)} connector · arrows to navigate, Enter to edit or run an action.`);
|
|
1707
|
+
}
|
|
1708
|
+
catch (error) {
|
|
1709
|
+
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
1710
|
+
}
|
|
1711
|
+
}, [baseUrl, connectorsDocument, loadConnectorsDocument]);
|
|
1712
|
+
const openRawConnectorsEditor = useCallback(async () => {
|
|
1713
|
+
const document = connectorsDocument ?? (await loadConnectorsDocument());
|
|
1714
|
+
setConnectorsDocument(document);
|
|
1715
|
+
await openConfigEditor(document.item);
|
|
1716
|
+
}, [connectorsDocument, loadConnectorsDocument, openConfigEditor]);
|
|
1717
|
+
const saveConnectorDraft = useCallback(async () => {
|
|
1718
|
+
if (!connectorsDocument) {
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
try {
|
|
1722
|
+
const structuredToSave = (() => {
|
|
1723
|
+
if (!selectedConnectorName) {
|
|
1724
|
+
return connectorsDocument.structured;
|
|
1725
|
+
}
|
|
1726
|
+
const merged = cloneStructured(connectorsDocument.savedStructured);
|
|
1727
|
+
merged[selectedConnectorName] = cloneStructured(asRecord(connectorsDocument.structured[selectedConnectorName]));
|
|
1728
|
+
return merged;
|
|
1729
|
+
})();
|
|
1730
|
+
const payload = await client.saveStructuredConfig(baseUrl, 'connectors', structuredToSave, connectorsDocument.revision);
|
|
1731
|
+
if (!payload.ok) {
|
|
1732
|
+
setStatusLine(payload.message || payload.errors?.[0] || 'Connector save failed.');
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
const refreshedDocument = await loadConnectorsDocument();
|
|
1736
|
+
setConnectorsDocument(refreshedDocument);
|
|
1737
|
+
await refresh(true, activeQuestIdRef.current);
|
|
1738
|
+
setStatusLine(`Saved ${selectedConnectorName ? connectorLabel(selectedConnectorName) : 'connectors'} settings.`);
|
|
1739
|
+
}
|
|
1740
|
+
catch (error) {
|
|
1741
|
+
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
1742
|
+
}
|
|
1743
|
+
}, [baseUrl, connectorsDocument, loadConnectorsDocument, refresh, selectedConnectorName]);
|
|
651
1744
|
const saveConfigEditor = useCallback(async (content) => {
|
|
652
1745
|
if (!configEditor) {
|
|
653
1746
|
return;
|
|
654
1747
|
}
|
|
655
1748
|
try {
|
|
1749
|
+
if (configEditor.kind === 'connector-field') {
|
|
1750
|
+
updateConnectorDraft(configEditor.connectorName, { [configEditor.fieldKey]: content });
|
|
1751
|
+
setInput('');
|
|
1752
|
+
setConfigEditor(null);
|
|
1753
|
+
setStatusLine(`Updated ${configEditor.fieldLabel} in draft. Save the connector to persist it.`);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
656
1756
|
if (configEditor.item.scope === 'global' && configEditor.item.configName) {
|
|
657
1757
|
const payload = await client.saveConfig(baseUrl, configEditor.item.configName, content, configEditor.revision);
|
|
658
1758
|
if (payload.ok === false) {
|
|
@@ -674,14 +1774,199 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
674
1774
|
}
|
|
675
1775
|
setInput('');
|
|
676
1776
|
setConfigEditor(null);
|
|
677
|
-
|
|
1777
|
+
setConfigView('files');
|
|
678
1778
|
setStatusLine(`Saved ${configEditor.item.title}`);
|
|
679
1779
|
await refresh(true, activeQuestId);
|
|
680
1780
|
}
|
|
681
1781
|
catch (error) {
|
|
682
1782
|
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
683
1783
|
}
|
|
684
|
-
}, [activeQuestId, baseUrl, configEditor, refresh, selectedQuestForConfig]);
|
|
1784
|
+
}, [activeQuestId, baseUrl, configEditor, refresh, selectedQuestForConfig, updateConnectorDraft]);
|
|
1785
|
+
const openConnectorFieldEditor = useCallback((fieldKey, fieldLabel, description, fieldKind) => {
|
|
1786
|
+
if (!selectedConnectorName) {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
const currentValue = String(selectedConnectorConfig[fieldKey] ?? '').trim();
|
|
1790
|
+
setConfigEditor({
|
|
1791
|
+
kind: 'connector-field',
|
|
1792
|
+
connectorName: selectedConnectorName,
|
|
1793
|
+
fieldKey,
|
|
1794
|
+
fieldLabel,
|
|
1795
|
+
description,
|
|
1796
|
+
fieldKind,
|
|
1797
|
+
content: currentValue,
|
|
1798
|
+
});
|
|
1799
|
+
setInput(currentValue);
|
|
1800
|
+
setStatusLine(`Editing ${fieldLabel} · Enter apply · Esc cancel`);
|
|
1801
|
+
}, [selectedConnectorConfig, selectedConnectorName]);
|
|
1802
|
+
const toggleConnectorBooleanField = useCallback((fieldKey, fieldLabel) => {
|
|
1803
|
+
if (!selectedConnectorName) {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const currentValue = Boolean(selectedConnectorConfig[fieldKey]);
|
|
1807
|
+
updateConnectorDraft(selectedConnectorName, { [fieldKey]: !currentValue });
|
|
1808
|
+
setStatusLine(`Updated ${fieldLabel} in draft. Save the connector to persist it.`);
|
|
1809
|
+
}, [selectedConnectorConfig, selectedConnectorName, updateConnectorDraft]);
|
|
1810
|
+
const handleConnectorAction = useCallback(async (actionId) => {
|
|
1811
|
+
if (!selectedConnectorName) {
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const bindTarget = parseConnectorBindActionId(actionId);
|
|
1815
|
+
if (bindTarget) {
|
|
1816
|
+
const questId = selectedQuestForConfig?.quest_id;
|
|
1817
|
+
if (!questId) {
|
|
1818
|
+
setStatusLine('Open a quest first, then bind the connector target to that quest.');
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
await client.updateQuestBindings(baseUrl, questId, {
|
|
1822
|
+
connector: bindTarget.connectorName,
|
|
1823
|
+
conversation_id: bindTarget.conversationId,
|
|
1824
|
+
force: true,
|
|
1825
|
+
});
|
|
1826
|
+
await refresh(true, questId);
|
|
1827
|
+
setStatusLine(`Bound ${connectorLabel(bindTarget.connectorName)} target to ${questId}.`);
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
const unbindConnectorName = parseConnectorUnbindActionId(actionId);
|
|
1831
|
+
if (unbindConnectorName) {
|
|
1832
|
+
const questId = selectedQuestForConfig?.quest_id;
|
|
1833
|
+
if (!questId) {
|
|
1834
|
+
setStatusLine('Open a quest first, then remove the connector binding from that quest.');
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
await client.updateQuestBindings(baseUrl, questId, {
|
|
1838
|
+
connector: unbindConnectorName,
|
|
1839
|
+
conversation_id: null,
|
|
1840
|
+
force: true,
|
|
1841
|
+
});
|
|
1842
|
+
await refresh(true, questId);
|
|
1843
|
+
setStatusLine(`Unbound ${connectorLabel(unbindConnectorName)} from ${questId}.`);
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
if (actionId === 'save-connector') {
|
|
1847
|
+
await saveConnectorDraft();
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (actionId === 'refresh-connector') {
|
|
1851
|
+
if (selectedConnectorName === 'weixin' || selectedConnectorName === 'qq' || selectedConnectorName === 'lingzhu') {
|
|
1852
|
+
const document = await loadConnectorsDocument();
|
|
1853
|
+
setConnectorsDocument(document);
|
|
1854
|
+
}
|
|
1855
|
+
await refresh(true, activeQuestIdRef.current);
|
|
1856
|
+
setStatusLine(`${connectorLabel(selectedConnectorName)} connector refreshed.`);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
if (actionId === 'generate-lingzhu-ak') {
|
|
1860
|
+
updateConnectorDraft('lingzhu', { auth_ak: createLingzhuAk() });
|
|
1861
|
+
setStatusLine('Generated a new Lingzhu Custom agent AK in draft.');
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (actionId === 'weixin-start-login') {
|
|
1865
|
+
const payload = await client.startWeixinQrLogin(baseUrl, Boolean(selectedConnectorSnapshot?.enabled));
|
|
1866
|
+
if (!payload.ok || !payload.session_key) {
|
|
1867
|
+
setStatusLine(payload.message || 'Failed to start Weixin QR login.');
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
setConfigView('weixin-qr');
|
|
1871
|
+
setConfigIndex(0);
|
|
1872
|
+
await setWeixinQrPayload({
|
|
1873
|
+
session_key: payload.session_key,
|
|
1874
|
+
status: 'wait',
|
|
1875
|
+
qrcode_content: payload.qrcode_content,
|
|
1876
|
+
qrcode_url: payload.qrcode_url,
|
|
1877
|
+
message: payload.message,
|
|
1878
|
+
});
|
|
1879
|
+
setStatusLine('Weixin QR login started. Scan the QR code with WeChat.');
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (actionId === 'open-raw-connectors') {
|
|
1883
|
+
await openRawConnectorsEditor();
|
|
1884
|
+
}
|
|
1885
|
+
}, [
|
|
1886
|
+
baseUrl,
|
|
1887
|
+
loadConnectorsDocument,
|
|
1888
|
+
openRawConnectorsEditor,
|
|
1889
|
+
refresh,
|
|
1890
|
+
saveConnectorDraft,
|
|
1891
|
+
selectedConnectorName,
|
|
1892
|
+
selectedConnectorSnapshot?.enabled,
|
|
1893
|
+
selectedQuestForConfig?.quest_id,
|
|
1894
|
+
setWeixinQrPayload,
|
|
1895
|
+
updateConnectorDraft,
|
|
1896
|
+
]);
|
|
1897
|
+
const handleConfigBrowseSelection = useCallback(async () => {
|
|
1898
|
+
if (!configPanel) {
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
if (configPanel.kind === 'root') {
|
|
1902
|
+
const selected = configPanel.items[configIndex];
|
|
1903
|
+
if (!selected) {
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (selected.id === 'connectors') {
|
|
1907
|
+
await openConnectorBrowser();
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
if (selected.id === 'global-files') {
|
|
1911
|
+
await openConfigFiles('global');
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
if (selected.id === 'quest-files') {
|
|
1915
|
+
await openConfigFiles('quest');
|
|
1916
|
+
}
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (configPanel.kind === 'files') {
|
|
1920
|
+
const selected = configPanel.items[configIndex] ?? null;
|
|
1921
|
+
if (!selected) {
|
|
1922
|
+
setStatusLine('No config file selected.');
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
await openConfigEditor(selected);
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
if (configPanel.kind === 'connector-list') {
|
|
1929
|
+
const selected = configPanel.items[configIndex] ?? null;
|
|
1930
|
+
if (!selected) {
|
|
1931
|
+
setStatusLine('No connector selected.');
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
await openConnectorDetail(selected.name);
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
if (configPanel.kind === 'connector-detail') {
|
|
1938
|
+
const selected = configPanel.items[configIndex] ?? null;
|
|
1939
|
+
if (!selected) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
if (selected.type === 'action') {
|
|
1943
|
+
if (!selected.disabled) {
|
|
1944
|
+
await handleConnectorAction(selected.id);
|
|
1945
|
+
}
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
if (selected.type === 'field') {
|
|
1949
|
+
if (!selected.editable) {
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
if (selected.fieldKind === 'boolean') {
|
|
1953
|
+
toggleConnectorBooleanField(selected.key, selected.label);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
openConnectorFieldEditor(selected.key, selected.label, selected.description, selected.fieldKind);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}, [
|
|
1960
|
+
configIndex,
|
|
1961
|
+
configPanel,
|
|
1962
|
+
handleConnectorAction,
|
|
1963
|
+
openConfigEditor,
|
|
1964
|
+
openConfigFiles,
|
|
1965
|
+
openConnectorBrowser,
|
|
1966
|
+
openConnectorDetail,
|
|
1967
|
+
openConnectorFieldEditor,
|
|
1968
|
+
toggleConnectorBooleanField,
|
|
1969
|
+
]);
|
|
685
1970
|
const closeQuestPanel = useCallback((nextStatus) => {
|
|
686
1971
|
setQuestPanelMode(null);
|
|
687
1972
|
if (nextStatus) {
|
|
@@ -713,10 +1998,10 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
713
1998
|
}, [baseUrl, closeQuestPanel, focusQuest, panelQuests, questPanelIndex, questPanelMode]);
|
|
714
1999
|
const cycleQuest = useCallback((direction) => {
|
|
715
2000
|
if (configMode === 'browse') {
|
|
716
|
-
if (
|
|
2001
|
+
if (configSelectionCount === 0) {
|
|
717
2002
|
return;
|
|
718
2003
|
}
|
|
719
|
-
setConfigIndex((previous) => (previous + direction +
|
|
2004
|
+
setConfigIndex((previous) => (previous + direction + configSelectionCount) % configSelectionCount);
|
|
720
2005
|
return;
|
|
721
2006
|
}
|
|
722
2007
|
if (questPanelMode) {
|
|
@@ -744,7 +2029,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
744
2029
|
const nextIndex = index < 0 ? 0 : (index + direction + quests.length) % quests.length;
|
|
745
2030
|
const nextQuestId = quests[nextIndex]?.quest_id ?? null;
|
|
746
2031
|
setBrowseQuestId(nextQuestId);
|
|
747
|
-
}, [activeQuestId, browseQuestId,
|
|
2032
|
+
}, [activeQuestId, browseQuestId, configMode, configSelectionCount, panelQuests, questPanelMode, quests]);
|
|
748
2033
|
useEffect(() => {
|
|
749
2034
|
void refresh(true, initialQuestId);
|
|
750
2035
|
}, [initialQuestId, refresh]);
|
|
@@ -852,6 +2137,63 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
852
2137
|
}
|
|
853
2138
|
};
|
|
854
2139
|
}, [activeQuestId, baseUrl, refresh]);
|
|
2140
|
+
useEffect(() => {
|
|
2141
|
+
if (configView !== 'weixin-qr' || !weixinQrState?.sessionKey) {
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
let cancelled = false;
|
|
2145
|
+
let timer = null;
|
|
2146
|
+
const poll = async () => {
|
|
2147
|
+
try {
|
|
2148
|
+
const payload = await client.waitWeixinQrLogin(baseUrl, weixinQrState.sessionKey, 1500);
|
|
2149
|
+
if (cancelled) {
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
await setWeixinQrPayload({
|
|
2153
|
+
session_key: payload.session_key || weixinQrState.sessionKey,
|
|
2154
|
+
status: payload.status,
|
|
2155
|
+
qrcode_content: payload.qrcode_content,
|
|
2156
|
+
qrcode_url: payload.qrcode_url,
|
|
2157
|
+
message: payload.message,
|
|
2158
|
+
});
|
|
2159
|
+
if (payload.connected) {
|
|
2160
|
+
const document = await loadConnectorsDocument();
|
|
2161
|
+
if (cancelled) {
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
setConnectorsDocument(document);
|
|
2165
|
+
await refresh(true, activeQuestIdRef.current);
|
|
2166
|
+
if (cancelled) {
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
setSelectedConnectorName('weixin');
|
|
2170
|
+
setConfigView('connector-detail');
|
|
2171
|
+
setConfigIndex(0);
|
|
2172
|
+
setStatusLine(payload.message || 'Weixin login succeeded and the connector config was saved.');
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
catch (error) {
|
|
2177
|
+
if (!cancelled) {
|
|
2178
|
+
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
if (!cancelled) {
|
|
2182
|
+
timer = setTimeout(() => {
|
|
2183
|
+
void poll();
|
|
2184
|
+
}, 1200);
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
timer = setTimeout(() => {
|
|
2188
|
+
void poll();
|
|
2189
|
+
}, 400);
|
|
2190
|
+
return () => {
|
|
2191
|
+
cancelled = true;
|
|
2192
|
+
if (timer) {
|
|
2193
|
+
clearTimeout(timer);
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
}, [baseUrl, configView, loadConnectorsDocument, refresh, setWeixinQrPayload, weixinQrState?.sessionKey]);
|
|
855
2197
|
const submit = useCallback(async (override) => {
|
|
856
2198
|
const rawText = override ?? input;
|
|
857
2199
|
if (configMode === 'edit' && configEditor) {
|
|
@@ -860,12 +2202,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
860
2202
|
}
|
|
861
2203
|
const text = rawText.trim();
|
|
862
2204
|
if (configMode === 'browse' && !text) {
|
|
863
|
-
|
|
864
|
-
if (!selected) {
|
|
865
|
-
setStatusLine('No config file selected.');
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
await openConfigEditor(selected);
|
|
2205
|
+
await handleConfigBrowseSelection();
|
|
869
2206
|
return;
|
|
870
2207
|
}
|
|
871
2208
|
if (configMode === 'browse' && !text.startsWith('/')) {
|
|
@@ -941,11 +2278,13 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
941
2278
|
return;
|
|
942
2279
|
}
|
|
943
2280
|
if (slash?.name === '/resume') {
|
|
944
|
-
|
|
2281
|
+
const target = slash.arg
|
|
2282
|
+
? resolveQuestToken(slash.arg, quests)
|
|
2283
|
+
: quests.find((quest) => quest.quest_id === (activeQuestId || browseQuestId)) ?? null;
|
|
2284
|
+
if (!slash.arg && !target) {
|
|
945
2285
|
openQuestPanel('resume');
|
|
946
2286
|
return;
|
|
947
2287
|
}
|
|
948
|
-
const target = resolveQuestToken(slash.arg, quests);
|
|
949
2288
|
if (!target) {
|
|
950
2289
|
setStatusLine(`Unknown quest · ${slash.arg}`);
|
|
951
2290
|
return;
|
|
@@ -969,7 +2308,43 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
969
2308
|
return;
|
|
970
2309
|
}
|
|
971
2310
|
if (slash?.name === '/config') {
|
|
972
|
-
|
|
2311
|
+
const arg = String(slash.arg || '').trim();
|
|
2312
|
+
if (!arg) {
|
|
2313
|
+
openConfigRoot();
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
const tokens = arg.split(/\s+/).filter(Boolean);
|
|
2317
|
+
const first = tokens[0]?.toLowerCase() || '';
|
|
2318
|
+
const second = tokens[1]?.toLowerCase() || '';
|
|
2319
|
+
const directConnector = resolveManagedConnectorName(first);
|
|
2320
|
+
const nestedConnector = first === 'connectors' ? resolveManagedConnectorName(second) : null;
|
|
2321
|
+
if (first === 'connectors' && nestedConnector) {
|
|
2322
|
+
await openConnectorBrowser(nestedConnector);
|
|
2323
|
+
await openConnectorDetail(nestedConnector);
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
if (directConnector) {
|
|
2327
|
+
await openConnectorBrowser(directConnector);
|
|
2328
|
+
await openConnectorDetail(directConnector);
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
if (first === 'connectors') {
|
|
2332
|
+
await openConnectorBrowser();
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
if (first === 'global') {
|
|
2336
|
+
await openConfigFiles('global');
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
if (first === 'quest') {
|
|
2340
|
+
await openConfigFiles('quest');
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
if (first === 'connectors.yaml') {
|
|
2344
|
+
await openConfigFiles('global', 'connectors');
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
await openConfigFiles('global', arg);
|
|
973
2348
|
return;
|
|
974
2349
|
}
|
|
975
2350
|
if (slash?.name === '/new') {
|
|
@@ -1105,15 +2480,17 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1105
2480
|
browseQuestId,
|
|
1106
2481
|
configEditor,
|
|
1107
2482
|
configIndex,
|
|
1108
|
-
configItems,
|
|
1109
2483
|
configMode,
|
|
1110
2484
|
closeQuestPanel,
|
|
1111
2485
|
focusQuest,
|
|
2486
|
+
handleConfigBrowseSelection,
|
|
1112
2487
|
handleQuestPanelSelection,
|
|
1113
2488
|
input,
|
|
1114
2489
|
leaveQuest,
|
|
1115
|
-
|
|
1116
|
-
|
|
2490
|
+
openConfigFiles,
|
|
2491
|
+
openConfigRoot,
|
|
2492
|
+
openConnectorBrowser,
|
|
2493
|
+
openConnectorDetail,
|
|
1117
2494
|
openQuestPanel,
|
|
1118
2495
|
questPanelMode,
|
|
1119
2496
|
quests,
|
|
@@ -1121,6 +2498,32 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1121
2498
|
replyTargetId,
|
|
1122
2499
|
saveConfigEditor,
|
|
1123
2500
|
]);
|
|
2501
|
+
const backFromConfigBrowse = useCallback(() => {
|
|
2502
|
+
if (!configView) {
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
if (configView === 'root') {
|
|
2506
|
+
closeConfigScreen('Config browser closed.');
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
if (configView === 'files' || configView === 'connector-list') {
|
|
2510
|
+
openConfigRoot();
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
if (configView === 'connector-detail') {
|
|
2514
|
+
setConfigView('connector-list');
|
|
2515
|
+
setConfigIndex(0);
|
|
2516
|
+
setSelectedConnectorName(null);
|
|
2517
|
+
setWeixinQrState(null);
|
|
2518
|
+
setStatusLine('Connectors · choose a connector with arrows and Enter.');
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
if (configView === 'weixin-qr') {
|
|
2522
|
+
setConfigView('connector-detail');
|
|
2523
|
+
setConfigIndex(0);
|
|
2524
|
+
setStatusLine('Back to Weixin connector details.');
|
|
2525
|
+
}
|
|
2526
|
+
}, [closeConfigScreen, configView, openConfigRoot]);
|
|
1124
2527
|
useInput((value, key) => {
|
|
1125
2528
|
const canBrowseSelection = configMode === 'browse' || Boolean(questPanelMode);
|
|
1126
2529
|
const canBrowseHomeQuests = !activeQuestId && input.length === 0;
|
|
@@ -1138,7 +2541,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1138
2541
|
return;
|
|
1139
2542
|
}
|
|
1140
2543
|
if (key.ctrl && value.toLowerCase() === 'g') {
|
|
1141
|
-
|
|
2544
|
+
openConfigRoot();
|
|
1142
2545
|
return;
|
|
1143
2546
|
}
|
|
1144
2547
|
if (key.ctrl && value.toLowerCase() === 'b') {
|
|
@@ -1152,14 +2555,13 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1152
2555
|
}
|
|
1153
2556
|
if (key.escape) {
|
|
1154
2557
|
if (configMode === 'edit') {
|
|
1155
|
-
setConfigMode('browse');
|
|
1156
2558
|
setConfigEditor(null);
|
|
1157
2559
|
setInput('');
|
|
1158
2560
|
setStatusLine('Config edit cancelled.');
|
|
1159
2561
|
return;
|
|
1160
2562
|
}
|
|
1161
2563
|
if (configMode === 'browse') {
|
|
1162
|
-
|
|
2564
|
+
backFromConfigBrowse();
|
|
1163
2565
|
return;
|
|
1164
2566
|
}
|
|
1165
2567
|
if (questPanelMode) {
|
|
@@ -1171,10 +2573,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1171
2573
|
}
|
|
1172
2574
|
if (submitRequested) {
|
|
1173
2575
|
if (configMode === 'browse' && input.trim().length === 0) {
|
|
1174
|
-
|
|
1175
|
-
if (selected) {
|
|
1176
|
-
void openConfigEditor(selected);
|
|
1177
|
-
}
|
|
2576
|
+
void handleConfigBrowseSelection();
|
|
1178
2577
|
return;
|
|
1179
2578
|
}
|
|
1180
2579
|
}
|
|
@@ -1187,12 +2586,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1187
2586
|
return;
|
|
1188
2587
|
}
|
|
1189
2588
|
});
|
|
1190
|
-
return (React.createElement(DefaultAppLayout, { baseUrl: baseUrl, quests: quests, activeQuestId: activeQuestId, browseQuestId: browseQuestId, configMode: configMode,
|
|
1191
|
-
? {
|
|
1192
|
-
item: configEditor.item,
|
|
1193
|
-
content: input,
|
|
1194
|
-
}
|
|
1195
|
-
: null, snapshot: activeQuest, session: session, connectors: connectors, history: history, pendingHistoryItems: pendingHistoryItems, input: input, connectionState: connectionState, statusLine: statusLine, suggestions: slashSuggestions, questPanelMode: questPanelMode, questPanelQuests: panelQuests, questPanelIndex: questPanelIndex, onQuestPanelMove: (direction) => {
|
|
2589
|
+
return (React.createElement(DefaultAppLayout, { baseUrl: baseUrl, quests: quests, activeQuestId: activeQuestId, browseQuestId: browseQuestId, configMode: configMode, configPanel: configPanel, snapshot: activeQuest, session: session, connectors: connectors, history: history, pendingHistoryItems: pendingHistoryItems, input: input, connectionState: connectionState, statusLine: statusLine, suggestions: slashSuggestions, questPanelMode: questPanelMode, questPanelQuests: panelQuests, questPanelIndex: questPanelIndex, onQuestPanelMove: (direction) => {
|
|
1196
2590
|
cycleQuest(direction);
|
|
1197
2591
|
}, onQuestPanelConfirm: () => {
|
|
1198
2592
|
void handleQuestPanelSelection();
|
|
@@ -1201,10 +2595,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1201
2595
|
}, onChange: setInput, onSubmit: (override) => {
|
|
1202
2596
|
const submitted = override ?? input;
|
|
1203
2597
|
if (configMode === 'browse' && !String(submitted).trim()) {
|
|
1204
|
-
|
|
1205
|
-
if (selected) {
|
|
1206
|
-
void openConfigEditor(selected);
|
|
1207
|
-
}
|
|
2598
|
+
void handleConfigBrowseSelection();
|
|
1208
2599
|
return;
|
|
1209
2600
|
}
|
|
1210
2601
|
if (questPanelMode && !String(submitted).trim()) {
|
|
@@ -1218,14 +2609,13 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1218
2609
|
void submit(override);
|
|
1219
2610
|
}, onCancel: () => {
|
|
1220
2611
|
if (configMode === 'edit') {
|
|
1221
|
-
setConfigMode('browse');
|
|
1222
2612
|
setConfigEditor(null);
|
|
1223
2613
|
setInput('');
|
|
1224
2614
|
setStatusLine('Config edit cancelled.');
|
|
1225
2615
|
return;
|
|
1226
2616
|
}
|
|
1227
2617
|
if (configMode === 'browse') {
|
|
1228
|
-
|
|
2618
|
+
backFromConfigBrowse();
|
|
1229
2619
|
return;
|
|
1230
2620
|
}
|
|
1231
2621
|
if (questPanelMode) {
|