@researai/deepscientist 1.5.13 → 1.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/assets/branding/logo-raster.png +0 -0
- package/bin/ds.js +134 -49
- package/docs/en/00_QUICK_START.md +2 -2
- package/docs/en/01_SETTINGS_REFERENCE.md +20 -4
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/en/05_TUI_GUIDE.md +466 -96
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
- package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/en/README.md +8 -0
- package/docs/zh/00_QUICK_START.md +2 -2
- package/docs/zh/01_SETTINGS_REFERENCE.md +20 -4
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/zh/05_TUI_GUIDE.md +465 -82
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
- package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/zh/README.md +8 -0
- package/install.sh +2 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/artifact/charts.py +567 -0
- package/src/deepscientist/artifact/guidance.py +50 -10
- package/src/deepscientist/artifact/metrics.py +228 -5
- package/src/deepscientist/artifact/schemas.py +3 -0
- package/src/deepscientist/artifact/service.py +4004 -538
- package/src/deepscientist/bash_exec/models.py +23 -0
- package/src/deepscientist/bash_exec/monitor.py +147 -67
- package/src/deepscientist/bash_exec/runtime.py +218 -156
- package/src/deepscientist/bash_exec/service.py +79 -64
- package/src/deepscientist/bash_exec/shells.py +87 -0
- package/src/deepscientist/bridges/connectors.py +51 -2
- package/src/deepscientist/config/models.py +6 -3
- package/src/deepscientist/config/service.py +7 -2
- package/src/deepscientist/connector/lingzhu_support.py +23 -4
- package/src/deepscientist/connector/weixin_support.py +122 -1
- package/src/deepscientist/daemon/api/handlers.py +75 -4
- package/src/deepscientist/daemon/api/router.py +1 -0
- package/src/deepscientist/daemon/app.py +869 -236
- package/src/deepscientist/doctor.py +51 -0
- package/src/deepscientist/file_lock.py +48 -0
- package/src/deepscientist/gitops/diff.py +167 -1
- package/src/deepscientist/mcp/server.py +331 -21
- package/src/deepscientist/process_control.py +161 -0
- package/src/deepscientist/prompts/builder.py +275 -491
- package/src/deepscientist/quest/service.py +2336 -145
- package/src/deepscientist/quest/stage_views.py +305 -29
- package/src/deepscientist/runners/base.py +2 -0
- package/src/deepscientist/runners/codex.py +88 -5
- package/src/deepscientist/runners/runtime_overrides.py +17 -1
- package/src/deepscientist/shared.py +6 -1
- package/src/prompts/contracts/shared_interaction.md +13 -4
- package/src/prompts/system.md +984 -1985
- package/src/skills/analysis-campaign/SKILL.md +31 -2
- package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
- package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
- package/src/skills/baseline/SKILL.md +267 -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/skills/decision/SKILL.md +19 -2
- package/src/skills/experiment/SKILL.md +8 -2
- package/src/skills/finalize/SKILL.md +18 -0
- package/src/skills/idea/SKILL.md +78 -0
- package/src/skills/idea/references/idea-generation-playbook.md +100 -0
- package/src/skills/idea/references/outline-seeding-example.md +60 -0
- package/src/skills/intake-audit/SKILL.md +1 -1
- package/src/skills/optimize/SKILL.md +1644 -0
- package/src/skills/rebuttal/SKILL.md +2 -1
- package/src/skills/review/SKILL.md +2 -1
- package/src/skills/write/SKILL.md +80 -12
- package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
- package/src/tui/dist/app/AppContainer.js +1445 -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-DDjbFnbt.js} +12 -12
- package/src/ui/dist/assets/{AnalysisPlugin-DeyzPEhV.js → AnalysisPlugin-Yb5IdmaU.js} +1 -1
- package/src/ui/dist/assets/CliPlugin-e64sreyu.js +31037 -0
- package/src/ui/dist/assets/{CodeEditorPlugin-B-xicq1e.js → CodeEditorPlugin-C4D2TIkU.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DT54ysXa.js → CodeViewerPlugin-BVoNZIvC.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-DQtKT-VD.js → DocViewerPlugin-CLChbllo.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-hqHbCfnv.js → GitDiffViewerPlugin-C4xeFyFQ.js} +20 -20
- package/src/ui/dist/assets/{ImageViewerPlugin-OcVo33jV.js → ImageViewerPlugin-OiMUAcLi.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-DdGwhEUV.js → LabCopilotPanel-BjD2ThQF.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-Ciz1gDaX.js → LabPlugin-DQPg-NrB.js} +2 -2
- package/src/ui/dist/assets/{LatexPlugin-BhmjNQRC.js → LatexPlugin-CI05XAV9.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-BzdVH9Bx.js → MarkdownViewerPlugin-DpeBLYZf.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DmyHspXt.js → MarketplacePlugin-DolE58Q2.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-BTVYRGkm.js → NotebookEditor-7Qm2rSWD.js} +11 -11
- package/src/ui/dist/assets/{NotebookEditor-BMXKrDRk.js → NotebookEditor-C1kWaxKi.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-CvcjJHXv.js → PdfLoader-BfOHw8Zw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DW2ej8Vk.js → PdfMarkdownPlugin-BulDREv1.js} +2 -2
- package/src/ui/dist/assets/{PdfViewerPlugin-CmlDxbhU.js → PdfViewerPlugin-C-daaOaL.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-DAjQZPSv.js → SearchPlugin-CjpaiJ3A.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-C-nVAZb_.js → TextViewerPlugin-BxIyqPQC.js} +5 -5
- package/src/ui/dist/assets/{VNCViewer-D7-dIYon.js → VNCViewer-HAg9mF7M.js} +10 -10
- package/src/ui/dist/assets/{bot-C_G4WtNI.js → bot-0DYntytV.js} +1 -1
- package/src/ui/dist/assets/{code-Cd7WfiWq.js → code-B20Slj_w.js} +1 -1
- package/src/ui/dist/assets/{file-content-B57zsL9y.js → file-content-DT24KFma.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DVoheLFq.js → file-diff-panel-DK13YPql.js} +1 -1
- package/src/ui/dist/assets/{file-socket-B5kXFxZP.js → file-socket-B4T2o4nR.js} +1 -1
- package/src/ui/dist/assets/{image-LLOjkMHF.js → image-DSeR_sDS.js} +1 -1
- package/src/ui/dist/assets/{index-hOUOWbW2.js → index-BrFje2Uk.js} +2 -2
- package/src/ui/dist/assets/{index-Dxa2eYMY.js → index-BwRJaoTl.js} +1 -1
- package/src/ui/dist/assets/{index-CLQauncb.js → index-D_E4281X.js} +5418 -28620
- package/src/ui/dist/assets/{index-C3r2iGrp.js → index-DnYB3xb1.js} +12 -12
- package/src/ui/dist/assets/{index-BQG-1s2o.css → index-G7AcWcMu.css} +43 -2
- package/src/ui/dist/assets/{monaco-BGGAEii3.js → monaco-LExaAN3Y.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DlEr1_y5.js → pdf-effect-queue-BJk5okWJ.js} +1 -1
- package/src/ui/dist/assets/{popover-CWJbJuYY.js → popover-D3Gg_FoV.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CRJiucYO.js → project-sync-C_ygLlVU.js} +1 -1
- package/src/ui/dist/assets/{select-CoHB7pvH.js → select-CpAK6uWm.js} +2 -2
- package/src/ui/dist/assets/{sigma-D5aJWR8J.js → sigma-DEccaSgk.js} +1 -1
- package/src/ui/dist/assets/{square-check-big-DUK_mnkS.js → square-check-big-uUfyVsbD.js} +1 -1
- package/src/ui/dist/assets/{trash-ChU3SEE3.js → trash-CXvwwSe8.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-BrJBV3tY.js → useCliAccess-Bnop4mgR.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-C2OQaVWc.js → useFileDiffOverlay-B8eUAX0I.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-C7Qqh-om.js → wrap-text-9vbOBpkW.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-rtX0FKya.js → zoom-out-BgVMmOW4.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/uv.lock +1 -1
- package/src/ui/dist/assets/CliPlugin-CB1YODQn.js +0 -5905
|
@@ -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,177 @@ 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 openConfigBrowser = useCallback((nextStatus) => {
|
|
1608
|
+
openConfigRoot(nextStatus);
|
|
1609
|
+
}, [openConfigRoot]);
|
|
1610
|
+
const openConnectorBrowser = useCallback(async (targetConnector) => {
|
|
1611
|
+
try {
|
|
1612
|
+
const document = await loadConnectorsDocument();
|
|
1613
|
+
const names = connectorNamesFromStructuredConfig(document.structured);
|
|
1614
|
+
const browseNames = names.length > 0 ? names : CONNECTOR_ORDER;
|
|
1615
|
+
setQuestPanelMode(null);
|
|
1616
|
+
setConfigView('connector-list');
|
|
1617
|
+
setConfigEditor(null);
|
|
1618
|
+
setConnectorsDocument(document);
|
|
1619
|
+
setSelectedConnectorName(null);
|
|
1620
|
+
setWeixinQrState(null);
|
|
1621
|
+
setInput('');
|
|
1622
|
+
if (targetConnector) {
|
|
1623
|
+
const directIndex = browseNames.findIndex((item) => item === targetConnector);
|
|
1624
|
+
setConfigIndex(directIndex >= 0 ? directIndex : 0);
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
setConfigIndex(0);
|
|
1628
|
+
setStatusLine('Connectors · choose a connector with arrows and Enter.');
|
|
1629
|
+
}
|
|
1630
|
+
catch (error) {
|
|
1631
|
+
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
1632
|
+
}
|
|
1633
|
+
}, [loadConnectorsDocument]);
|
|
1634
|
+
const updateConnectorDraft = useCallback((connectorName, patch) => {
|
|
1635
|
+
setConnectorsDocument((current) => {
|
|
1636
|
+
if (!current) {
|
|
1637
|
+
return current;
|
|
1638
|
+
}
|
|
1639
|
+
const nextStructured = cloneStructured(current.structured);
|
|
1640
|
+
const currentConnector = asRecord(nextStructured[connectorName]);
|
|
1641
|
+
nextStructured[connectorName] = {
|
|
1642
|
+
...currentConnector,
|
|
1643
|
+
...patch,
|
|
1644
|
+
};
|
|
1645
|
+
return {
|
|
1646
|
+
...current,
|
|
1647
|
+
structured: nextStructured,
|
|
1648
|
+
};
|
|
1649
|
+
});
|
|
1650
|
+
}, []);
|
|
1651
|
+
const openConnectorDetail = useCallback(async (connectorName) => {
|
|
1652
|
+
try {
|
|
1653
|
+
let currentDocument = connectorsDocument ?? (await loadConnectorsDocument());
|
|
1654
|
+
if (connectorName === 'lingzhu') {
|
|
1655
|
+
const currentLingzhu = asRecord(currentDocument.structured.lingzhu);
|
|
1656
|
+
const patch = {};
|
|
1657
|
+
if (!resolveLingzhuAuthAk(currentLingzhu.auth_ak)) {
|
|
1658
|
+
patch.auth_ak = createLingzhuAk();
|
|
1659
|
+
}
|
|
1660
|
+
if (!String(currentLingzhu.agent_id || '').trim()) {
|
|
1661
|
+
patch.agent_id = LINGZHU_PUBLIC_AGENT_ID;
|
|
1662
|
+
}
|
|
1663
|
+
if (!String(currentLingzhu.local_host || '').trim()) {
|
|
1664
|
+
patch.local_host = '127.0.0.1';
|
|
1665
|
+
}
|
|
1666
|
+
if (!String(currentLingzhu.gateway_port || '').trim()) {
|
|
1667
|
+
try {
|
|
1668
|
+
patch.gateway_port = String(new URL(baseUrl).port || '20999');
|
|
1669
|
+
}
|
|
1670
|
+
catch {
|
|
1671
|
+
patch.gateway_port = '20999';
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
if (!String(currentLingzhu.public_base_url || '').trim()) {
|
|
1675
|
+
try {
|
|
1676
|
+
const currentBaseUrl = new URL(baseUrl);
|
|
1677
|
+
if (currentBaseUrl.hostname === '0.0.0.0') {
|
|
1678
|
+
currentBaseUrl.hostname = '127.0.0.1';
|
|
1679
|
+
}
|
|
1680
|
+
const resolvedBaseUrl = currentBaseUrl.toString().replace(/\/$/, '');
|
|
1681
|
+
if (looksLikePublicBaseUrl(resolvedBaseUrl)) {
|
|
1682
|
+
patch.public_base_url = resolvedBaseUrl;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
catch {
|
|
1686
|
+
// ignore parse failures
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (Object.keys(patch).length > 0) {
|
|
1690
|
+
currentDocument = {
|
|
1691
|
+
...currentDocument,
|
|
1692
|
+
structured: {
|
|
1693
|
+
...cloneStructured(currentDocument.structured),
|
|
1694
|
+
lingzhu: {
|
|
1695
|
+
...currentLingzhu,
|
|
1696
|
+
...patch,
|
|
1697
|
+
},
|
|
1698
|
+
},
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
setConnectorsDocument(currentDocument);
|
|
1703
|
+
setSelectedConnectorName(connectorName);
|
|
1704
|
+
setConfigView('connector-detail');
|
|
1705
|
+
setConfigEditor(null);
|
|
1706
|
+
setWeixinQrState(null);
|
|
1707
|
+
setConfigIndex(0);
|
|
1708
|
+
setInput('');
|
|
1709
|
+
setStatusLine(`${connectorLabel(connectorName)} connector · arrows to navigate, Enter to edit or run an action.`);
|
|
1710
|
+
}
|
|
1711
|
+
catch (error) {
|
|
1712
|
+
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
1713
|
+
}
|
|
1714
|
+
}, [baseUrl, connectorsDocument, loadConnectorsDocument]);
|
|
1715
|
+
const openRawConnectorsEditor = useCallback(async () => {
|
|
1716
|
+
const document = connectorsDocument ?? (await loadConnectorsDocument());
|
|
1717
|
+
setConnectorsDocument(document);
|
|
1718
|
+
await openConfigEditor(document.item);
|
|
1719
|
+
}, [connectorsDocument, loadConnectorsDocument, openConfigEditor]);
|
|
1720
|
+
const saveConnectorDraft = useCallback(async () => {
|
|
1721
|
+
if (!connectorsDocument) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
try {
|
|
1725
|
+
const structuredToSave = (() => {
|
|
1726
|
+
if (!selectedConnectorName) {
|
|
1727
|
+
return connectorsDocument.structured;
|
|
1728
|
+
}
|
|
1729
|
+
const merged = cloneStructured(connectorsDocument.savedStructured);
|
|
1730
|
+
merged[selectedConnectorName] = cloneStructured(asRecord(connectorsDocument.structured[selectedConnectorName]));
|
|
1731
|
+
return merged;
|
|
1732
|
+
})();
|
|
1733
|
+
const payload = await client.saveStructuredConfig(baseUrl, 'connectors', structuredToSave, connectorsDocument.revision);
|
|
1734
|
+
if (!payload.ok) {
|
|
1735
|
+
setStatusLine(payload.message || payload.errors?.[0] || 'Connector save failed.');
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
const refreshedDocument = await loadConnectorsDocument();
|
|
1739
|
+
setConnectorsDocument(refreshedDocument);
|
|
1740
|
+
await refresh(true, activeQuestIdRef.current);
|
|
1741
|
+
setStatusLine(`Saved ${selectedConnectorName ? connectorLabel(selectedConnectorName) : 'connectors'} settings.`);
|
|
1742
|
+
}
|
|
1743
|
+
catch (error) {
|
|
1744
|
+
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
1745
|
+
}
|
|
1746
|
+
}, [baseUrl, connectorsDocument, loadConnectorsDocument, refresh, selectedConnectorName]);
|
|
651
1747
|
const saveConfigEditor = useCallback(async (content) => {
|
|
652
1748
|
if (!configEditor) {
|
|
653
1749
|
return;
|
|
654
1750
|
}
|
|
655
1751
|
try {
|
|
1752
|
+
if (configEditor.kind === 'connector-field') {
|
|
1753
|
+
updateConnectorDraft(configEditor.connectorName, { [configEditor.fieldKey]: content });
|
|
1754
|
+
setInput('');
|
|
1755
|
+
setConfigEditor(null);
|
|
1756
|
+
setStatusLine(`Updated ${configEditor.fieldLabel} in draft. Save the connector to persist it.`);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
656
1759
|
if (configEditor.item.scope === 'global' && configEditor.item.configName) {
|
|
657
1760
|
const payload = await client.saveConfig(baseUrl, configEditor.item.configName, content, configEditor.revision);
|
|
658
1761
|
if (payload.ok === false) {
|
|
@@ -674,14 +1777,199 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
674
1777
|
}
|
|
675
1778
|
setInput('');
|
|
676
1779
|
setConfigEditor(null);
|
|
677
|
-
|
|
1780
|
+
setConfigView('files');
|
|
678
1781
|
setStatusLine(`Saved ${configEditor.item.title}`);
|
|
679
1782
|
await refresh(true, activeQuestId);
|
|
680
1783
|
}
|
|
681
1784
|
catch (error) {
|
|
682
1785
|
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
683
1786
|
}
|
|
684
|
-
}, [activeQuestId, baseUrl, configEditor, refresh, selectedQuestForConfig]);
|
|
1787
|
+
}, [activeQuestId, baseUrl, configEditor, refresh, selectedQuestForConfig, updateConnectorDraft]);
|
|
1788
|
+
const openConnectorFieldEditor = useCallback((fieldKey, fieldLabel, description, fieldKind) => {
|
|
1789
|
+
if (!selectedConnectorName) {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const currentValue = String(selectedConnectorConfig[fieldKey] ?? '').trim();
|
|
1793
|
+
setConfigEditor({
|
|
1794
|
+
kind: 'connector-field',
|
|
1795
|
+
connectorName: selectedConnectorName,
|
|
1796
|
+
fieldKey,
|
|
1797
|
+
fieldLabel,
|
|
1798
|
+
description,
|
|
1799
|
+
fieldKind,
|
|
1800
|
+
content: currentValue,
|
|
1801
|
+
});
|
|
1802
|
+
setInput(currentValue);
|
|
1803
|
+
setStatusLine(`Editing ${fieldLabel} · Enter apply · Esc cancel`);
|
|
1804
|
+
}, [selectedConnectorConfig, selectedConnectorName]);
|
|
1805
|
+
const toggleConnectorBooleanField = useCallback((fieldKey, fieldLabel) => {
|
|
1806
|
+
if (!selectedConnectorName) {
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const currentValue = Boolean(selectedConnectorConfig[fieldKey]);
|
|
1810
|
+
updateConnectorDraft(selectedConnectorName, { [fieldKey]: !currentValue });
|
|
1811
|
+
setStatusLine(`Updated ${fieldLabel} in draft. Save the connector to persist it.`);
|
|
1812
|
+
}, [selectedConnectorConfig, selectedConnectorName, updateConnectorDraft]);
|
|
1813
|
+
const handleConnectorAction = useCallback(async (actionId) => {
|
|
1814
|
+
if (!selectedConnectorName) {
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
const bindTarget = parseConnectorBindActionId(actionId);
|
|
1818
|
+
if (bindTarget) {
|
|
1819
|
+
const questId = selectedQuestForConfig?.quest_id;
|
|
1820
|
+
if (!questId) {
|
|
1821
|
+
setStatusLine('Open a quest first, then bind the connector target to that quest.');
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
await client.updateQuestBindings(baseUrl, questId, {
|
|
1825
|
+
connector: bindTarget.connectorName,
|
|
1826
|
+
conversation_id: bindTarget.conversationId,
|
|
1827
|
+
force: true,
|
|
1828
|
+
});
|
|
1829
|
+
await refresh(true, questId);
|
|
1830
|
+
setStatusLine(`Bound ${connectorLabel(bindTarget.connectorName)} target to ${questId}.`);
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
const unbindConnectorName = parseConnectorUnbindActionId(actionId);
|
|
1834
|
+
if (unbindConnectorName) {
|
|
1835
|
+
const questId = selectedQuestForConfig?.quest_id;
|
|
1836
|
+
if (!questId) {
|
|
1837
|
+
setStatusLine('Open a quest first, then remove the connector binding from that quest.');
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
await client.updateQuestBindings(baseUrl, questId, {
|
|
1841
|
+
connector: unbindConnectorName,
|
|
1842
|
+
conversation_id: null,
|
|
1843
|
+
force: true,
|
|
1844
|
+
});
|
|
1845
|
+
await refresh(true, questId);
|
|
1846
|
+
setStatusLine(`Unbound ${connectorLabel(unbindConnectorName)} from ${questId}.`);
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
if (actionId === 'save-connector') {
|
|
1850
|
+
await saveConnectorDraft();
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
if (actionId === 'refresh-connector') {
|
|
1854
|
+
if (selectedConnectorName === 'weixin' || selectedConnectorName === 'qq' || selectedConnectorName === 'lingzhu') {
|
|
1855
|
+
const document = await loadConnectorsDocument();
|
|
1856
|
+
setConnectorsDocument(document);
|
|
1857
|
+
}
|
|
1858
|
+
await refresh(true, activeQuestIdRef.current);
|
|
1859
|
+
setStatusLine(`${connectorLabel(selectedConnectorName)} connector refreshed.`);
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
if (actionId === 'generate-lingzhu-ak') {
|
|
1863
|
+
updateConnectorDraft('lingzhu', { auth_ak: createLingzhuAk() });
|
|
1864
|
+
setStatusLine('Generated a new Lingzhu Custom agent AK in draft.');
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
if (actionId === 'weixin-start-login') {
|
|
1868
|
+
const payload = await client.startWeixinQrLogin(baseUrl, Boolean(selectedConnectorSnapshot?.enabled));
|
|
1869
|
+
if (!payload.ok || !payload.session_key) {
|
|
1870
|
+
setStatusLine(payload.message || 'Failed to start Weixin QR login.');
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
setConfigView('weixin-qr');
|
|
1874
|
+
setConfigIndex(0);
|
|
1875
|
+
await setWeixinQrPayload({
|
|
1876
|
+
session_key: payload.session_key,
|
|
1877
|
+
status: 'wait',
|
|
1878
|
+
qrcode_content: payload.qrcode_content,
|
|
1879
|
+
qrcode_url: payload.qrcode_url,
|
|
1880
|
+
message: payload.message,
|
|
1881
|
+
});
|
|
1882
|
+
setStatusLine('Weixin QR login started. Scan the QR code with WeChat.');
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (actionId === 'open-raw-connectors') {
|
|
1886
|
+
await openRawConnectorsEditor();
|
|
1887
|
+
}
|
|
1888
|
+
}, [
|
|
1889
|
+
baseUrl,
|
|
1890
|
+
loadConnectorsDocument,
|
|
1891
|
+
openRawConnectorsEditor,
|
|
1892
|
+
refresh,
|
|
1893
|
+
saveConnectorDraft,
|
|
1894
|
+
selectedConnectorName,
|
|
1895
|
+
selectedConnectorSnapshot?.enabled,
|
|
1896
|
+
selectedQuestForConfig?.quest_id,
|
|
1897
|
+
setWeixinQrPayload,
|
|
1898
|
+
updateConnectorDraft,
|
|
1899
|
+
]);
|
|
1900
|
+
const handleConfigBrowseSelection = useCallback(async () => {
|
|
1901
|
+
if (!configPanel) {
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
if (configPanel.kind === 'root') {
|
|
1905
|
+
const selected = configPanel.items[configIndex];
|
|
1906
|
+
if (!selected) {
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
if (selected.id === 'connectors') {
|
|
1910
|
+
await openConnectorBrowser();
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
if (selected.id === 'global-files') {
|
|
1914
|
+
await openConfigFiles('global');
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (selected.id === 'quest-files') {
|
|
1918
|
+
await openConfigFiles('quest');
|
|
1919
|
+
}
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
if (configPanel.kind === 'files') {
|
|
1923
|
+
const selected = configPanel.items[configIndex] ?? null;
|
|
1924
|
+
if (!selected) {
|
|
1925
|
+
setStatusLine('No config file selected.');
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
await openConfigEditor(selected);
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
if (configPanel.kind === 'connector-list') {
|
|
1932
|
+
const selected = configPanel.items[configIndex] ?? null;
|
|
1933
|
+
if (!selected) {
|
|
1934
|
+
setStatusLine('No connector selected.');
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
await openConnectorDetail(selected.name);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
if (configPanel.kind === 'connector-detail') {
|
|
1941
|
+
const selected = configPanel.items[configIndex] ?? null;
|
|
1942
|
+
if (!selected) {
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
if (selected.type === 'action') {
|
|
1946
|
+
if (!selected.disabled) {
|
|
1947
|
+
await handleConnectorAction(selected.id);
|
|
1948
|
+
}
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
if (selected.type === 'field') {
|
|
1952
|
+
if (!selected.editable) {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
if (selected.fieldKind === 'boolean') {
|
|
1956
|
+
toggleConnectorBooleanField(selected.key, selected.label);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
openConnectorFieldEditor(selected.key, selected.label, selected.description, selected.fieldKind);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}, [
|
|
1963
|
+
configIndex,
|
|
1964
|
+
configPanel,
|
|
1965
|
+
handleConnectorAction,
|
|
1966
|
+
openConfigEditor,
|
|
1967
|
+
openConfigFiles,
|
|
1968
|
+
openConnectorBrowser,
|
|
1969
|
+
openConnectorDetail,
|
|
1970
|
+
openConnectorFieldEditor,
|
|
1971
|
+
toggleConnectorBooleanField,
|
|
1972
|
+
]);
|
|
685
1973
|
const closeQuestPanel = useCallback((nextStatus) => {
|
|
686
1974
|
setQuestPanelMode(null);
|
|
687
1975
|
if (nextStatus) {
|
|
@@ -713,10 +2001,10 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
713
2001
|
}, [baseUrl, closeQuestPanel, focusQuest, panelQuests, questPanelIndex, questPanelMode]);
|
|
714
2002
|
const cycleQuest = useCallback((direction) => {
|
|
715
2003
|
if (configMode === 'browse') {
|
|
716
|
-
if (
|
|
2004
|
+
if (configSelectionCount === 0) {
|
|
717
2005
|
return;
|
|
718
2006
|
}
|
|
719
|
-
setConfigIndex((previous) => (previous + direction +
|
|
2007
|
+
setConfigIndex((previous) => (previous + direction + configSelectionCount) % configSelectionCount);
|
|
720
2008
|
return;
|
|
721
2009
|
}
|
|
722
2010
|
if (questPanelMode) {
|
|
@@ -744,7 +2032,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
744
2032
|
const nextIndex = index < 0 ? 0 : (index + direction + quests.length) % quests.length;
|
|
745
2033
|
const nextQuestId = quests[nextIndex]?.quest_id ?? null;
|
|
746
2034
|
setBrowseQuestId(nextQuestId);
|
|
747
|
-
}, [activeQuestId, browseQuestId,
|
|
2035
|
+
}, [activeQuestId, browseQuestId, configMode, configSelectionCount, panelQuests, questPanelMode, quests]);
|
|
748
2036
|
useEffect(() => {
|
|
749
2037
|
void refresh(true, initialQuestId);
|
|
750
2038
|
}, [initialQuestId, refresh]);
|
|
@@ -852,6 +2140,63 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
852
2140
|
}
|
|
853
2141
|
};
|
|
854
2142
|
}, [activeQuestId, baseUrl, refresh]);
|
|
2143
|
+
useEffect(() => {
|
|
2144
|
+
if (configView !== 'weixin-qr' || !weixinQrState?.sessionKey) {
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
let cancelled = false;
|
|
2148
|
+
let timer = null;
|
|
2149
|
+
const poll = async () => {
|
|
2150
|
+
try {
|
|
2151
|
+
const payload = await client.waitWeixinQrLogin(baseUrl, weixinQrState.sessionKey, 1500);
|
|
2152
|
+
if (cancelled) {
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
await setWeixinQrPayload({
|
|
2156
|
+
session_key: payload.session_key || weixinQrState.sessionKey,
|
|
2157
|
+
status: payload.status,
|
|
2158
|
+
qrcode_content: payload.qrcode_content,
|
|
2159
|
+
qrcode_url: payload.qrcode_url,
|
|
2160
|
+
message: payload.message,
|
|
2161
|
+
});
|
|
2162
|
+
if (payload.connected) {
|
|
2163
|
+
const document = await loadConnectorsDocument();
|
|
2164
|
+
if (cancelled) {
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
setConnectorsDocument(document);
|
|
2168
|
+
await refresh(true, activeQuestIdRef.current);
|
|
2169
|
+
if (cancelled) {
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
setSelectedConnectorName('weixin');
|
|
2173
|
+
setConfigView('connector-detail');
|
|
2174
|
+
setConfigIndex(0);
|
|
2175
|
+
setStatusLine(payload.message || 'Weixin login succeeded and the connector config was saved.');
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
catch (error) {
|
|
2180
|
+
if (!cancelled) {
|
|
2181
|
+
setStatusLine(error instanceof Error ? error.message : String(error));
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
if (!cancelled) {
|
|
2185
|
+
timer = setTimeout(() => {
|
|
2186
|
+
void poll();
|
|
2187
|
+
}, 1200);
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
timer = setTimeout(() => {
|
|
2191
|
+
void poll();
|
|
2192
|
+
}, 400);
|
|
2193
|
+
return () => {
|
|
2194
|
+
cancelled = true;
|
|
2195
|
+
if (timer) {
|
|
2196
|
+
clearTimeout(timer);
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
}, [baseUrl, configView, loadConnectorsDocument, refresh, setWeixinQrPayload, weixinQrState?.sessionKey]);
|
|
855
2200
|
const submit = useCallback(async (override) => {
|
|
856
2201
|
const rawText = override ?? input;
|
|
857
2202
|
if (configMode === 'edit' && configEditor) {
|
|
@@ -860,12 +2205,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
860
2205
|
}
|
|
861
2206
|
const text = rawText.trim();
|
|
862
2207
|
if (configMode === 'browse' && !text) {
|
|
863
|
-
|
|
864
|
-
if (!selected) {
|
|
865
|
-
setStatusLine('No config file selected.');
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
await openConfigEditor(selected);
|
|
2208
|
+
await handleConfigBrowseSelection();
|
|
869
2209
|
return;
|
|
870
2210
|
}
|
|
871
2211
|
if (configMode === 'browse' && !text.startsWith('/')) {
|
|
@@ -941,11 +2281,13 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
941
2281
|
return;
|
|
942
2282
|
}
|
|
943
2283
|
if (slash?.name === '/resume') {
|
|
944
|
-
|
|
2284
|
+
const target = slash.arg
|
|
2285
|
+
? resolveQuestToken(slash.arg, quests)
|
|
2286
|
+
: quests.find((quest) => quest.quest_id === (activeQuestId || browseQuestId)) ?? null;
|
|
2287
|
+
if (!slash.arg && !target) {
|
|
945
2288
|
openQuestPanel('resume');
|
|
946
2289
|
return;
|
|
947
2290
|
}
|
|
948
|
-
const target = resolveQuestToken(slash.arg, quests);
|
|
949
2291
|
if (!target) {
|
|
950
2292
|
setStatusLine(`Unknown quest · ${slash.arg}`);
|
|
951
2293
|
return;
|
|
@@ -969,7 +2311,43 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
969
2311
|
return;
|
|
970
2312
|
}
|
|
971
2313
|
if (slash?.name === '/config') {
|
|
972
|
-
|
|
2314
|
+
const arg = String(slash.arg || '').trim();
|
|
2315
|
+
if (!arg) {
|
|
2316
|
+
openConfigRoot();
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
const tokens = arg.split(/\s+/).filter(Boolean);
|
|
2320
|
+
const first = tokens[0]?.toLowerCase() || '';
|
|
2321
|
+
const second = tokens[1]?.toLowerCase() || '';
|
|
2322
|
+
const directConnector = resolveManagedConnectorName(first);
|
|
2323
|
+
const nestedConnector = first === 'connectors' ? resolveManagedConnectorName(second) : null;
|
|
2324
|
+
if (first === 'connectors' && nestedConnector) {
|
|
2325
|
+
await openConnectorBrowser(nestedConnector);
|
|
2326
|
+
await openConnectorDetail(nestedConnector);
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
if (directConnector) {
|
|
2330
|
+
await openConnectorBrowser(directConnector);
|
|
2331
|
+
await openConnectorDetail(directConnector);
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
if (first === 'connectors') {
|
|
2335
|
+
await openConnectorBrowser();
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
if (first === 'global') {
|
|
2339
|
+
await openConfigFiles('global');
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
if (first === 'quest') {
|
|
2343
|
+
await openConfigFiles('quest');
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
if (first === 'connectors.yaml') {
|
|
2347
|
+
await openConfigFiles('global', 'connectors');
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
await openConfigFiles('global', arg);
|
|
973
2351
|
return;
|
|
974
2352
|
}
|
|
975
2353
|
if (slash?.name === '/new') {
|
|
@@ -1105,15 +2483,17 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1105
2483
|
browseQuestId,
|
|
1106
2484
|
configEditor,
|
|
1107
2485
|
configIndex,
|
|
1108
|
-
configItems,
|
|
1109
2486
|
configMode,
|
|
1110
2487
|
closeQuestPanel,
|
|
1111
2488
|
focusQuest,
|
|
2489
|
+
handleConfigBrowseSelection,
|
|
1112
2490
|
handleQuestPanelSelection,
|
|
1113
2491
|
input,
|
|
1114
2492
|
leaveQuest,
|
|
1115
|
-
|
|
1116
|
-
|
|
2493
|
+
openConfigFiles,
|
|
2494
|
+
openConfigRoot,
|
|
2495
|
+
openConnectorBrowser,
|
|
2496
|
+
openConnectorDetail,
|
|
1117
2497
|
openQuestPanel,
|
|
1118
2498
|
questPanelMode,
|
|
1119
2499
|
quests,
|
|
@@ -1121,6 +2501,32 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1121
2501
|
replyTargetId,
|
|
1122
2502
|
saveConfigEditor,
|
|
1123
2503
|
]);
|
|
2504
|
+
const backFromConfigBrowse = useCallback(() => {
|
|
2505
|
+
if (!configView) {
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
if (configView === 'root') {
|
|
2509
|
+
closeConfigScreen('Config browser closed.');
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
if (configView === 'files' || configView === 'connector-list') {
|
|
2513
|
+
openConfigRoot();
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
if (configView === 'connector-detail') {
|
|
2517
|
+
setConfigView('connector-list');
|
|
2518
|
+
setConfigIndex(0);
|
|
2519
|
+
setSelectedConnectorName(null);
|
|
2520
|
+
setWeixinQrState(null);
|
|
2521
|
+
setStatusLine('Connectors · choose a connector with arrows and Enter.');
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
if (configView === 'weixin-qr') {
|
|
2525
|
+
setConfigView('connector-detail');
|
|
2526
|
+
setConfigIndex(0);
|
|
2527
|
+
setStatusLine('Back to Weixin connector details.');
|
|
2528
|
+
}
|
|
2529
|
+
}, [closeConfigScreen, configView, openConfigRoot]);
|
|
1124
2530
|
useInput((value, key) => {
|
|
1125
2531
|
const canBrowseSelection = configMode === 'browse' || Boolean(questPanelMode);
|
|
1126
2532
|
const canBrowseHomeQuests = !activeQuestId && input.length === 0;
|
|
@@ -1138,7 +2544,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1138
2544
|
return;
|
|
1139
2545
|
}
|
|
1140
2546
|
if (key.ctrl && value.toLowerCase() === 'g') {
|
|
1141
|
-
|
|
2547
|
+
openConfigRoot();
|
|
1142
2548
|
return;
|
|
1143
2549
|
}
|
|
1144
2550
|
if (key.ctrl && value.toLowerCase() === 'b') {
|
|
@@ -1152,14 +2558,13 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1152
2558
|
}
|
|
1153
2559
|
if (key.escape) {
|
|
1154
2560
|
if (configMode === 'edit') {
|
|
1155
|
-
setConfigMode('browse');
|
|
1156
2561
|
setConfigEditor(null);
|
|
1157
2562
|
setInput('');
|
|
1158
2563
|
setStatusLine('Config edit cancelled.');
|
|
1159
2564
|
return;
|
|
1160
2565
|
}
|
|
1161
2566
|
if (configMode === 'browse') {
|
|
1162
|
-
|
|
2567
|
+
backFromConfigBrowse();
|
|
1163
2568
|
return;
|
|
1164
2569
|
}
|
|
1165
2570
|
if (questPanelMode) {
|
|
@@ -1171,10 +2576,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1171
2576
|
}
|
|
1172
2577
|
if (submitRequested) {
|
|
1173
2578
|
if (configMode === 'browse' && input.trim().length === 0) {
|
|
1174
|
-
|
|
1175
|
-
if (selected) {
|
|
1176
|
-
void openConfigEditor(selected);
|
|
1177
|
-
}
|
|
2579
|
+
void handleConfigBrowseSelection();
|
|
1178
2580
|
return;
|
|
1179
2581
|
}
|
|
1180
2582
|
}
|
|
@@ -1187,12 +2589,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1187
2589
|
return;
|
|
1188
2590
|
}
|
|
1189
2591
|
});
|
|
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) => {
|
|
2592
|
+
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
2593
|
cycleQuest(direction);
|
|
1197
2594
|
}, onQuestPanelConfirm: () => {
|
|
1198
2595
|
void handleQuestPanelSelection();
|
|
@@ -1201,10 +2598,7 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1201
2598
|
}, onChange: setInput, onSubmit: (override) => {
|
|
1202
2599
|
const submitted = override ?? input;
|
|
1203
2600
|
if (configMode === 'browse' && !String(submitted).trim()) {
|
|
1204
|
-
|
|
1205
|
-
if (selected) {
|
|
1206
|
-
void openConfigEditor(selected);
|
|
1207
|
-
}
|
|
2601
|
+
void handleConfigBrowseSelection();
|
|
1208
2602
|
return;
|
|
1209
2603
|
}
|
|
1210
2604
|
if (questPanelMode && !String(submitted).trim()) {
|
|
@@ -1218,14 +2612,13 @@ export const AppContainer = ({ baseUrl, initialQuestId = null, }) => {
|
|
|
1218
2612
|
void submit(override);
|
|
1219
2613
|
}, onCancel: () => {
|
|
1220
2614
|
if (configMode === 'edit') {
|
|
1221
|
-
setConfigMode('browse');
|
|
1222
2615
|
setConfigEditor(null);
|
|
1223
2616
|
setInput('');
|
|
1224
2617
|
setStatusLine('Config edit cancelled.');
|
|
1225
2618
|
return;
|
|
1226
2619
|
}
|
|
1227
2620
|
if (configMode === 'browse') {
|
|
1228
|
-
|
|
2621
|
+
backFromConfigBrowse();
|
|
1229
2622
|
return;
|
|
1230
2623
|
}
|
|
1231
2624
|
if (questPanelMode) {
|