@xcanwin/manyoyo 5.9.2 → 5.9.11
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 +1 -0
- package/bin/manyoyo.js +19 -5
- package/lib/web/frontend/app.css +121 -55
- package/lib/web/frontend/app.html +16 -9
- package/lib/web/frontend/app.js +320 -46
- package/lib/web/frontend/codemirror-entry.js +13 -0
- package/lib/web/frontend/codemirror.bundle.js +13 -0
- package/lib/web/frontend/file-browser.js +220 -29
- package/lib/web/server.js +230 -15
- package/package.json +1 -1
package/lib/web/server.js
CHANGED
|
@@ -34,7 +34,8 @@ const WEB_TERMINAL_MIN_ROWS = 12;
|
|
|
34
34
|
const WEB_AGENT_CONTEXT_MAX_MESSAGES = 24;
|
|
35
35
|
const WEB_AGENT_CONTEXT_MAX_CHARS = 6000;
|
|
36
36
|
const WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS = 600;
|
|
37
|
-
const WEB_FILE_PREVIEW_MAX_BYTES =
|
|
37
|
+
const WEB_FILE_PREVIEW_MAX_BYTES = 512 * 1024;
|
|
38
|
+
const WEB_FILE_EDIT_MAX_BYTES = 2 * 1024 * 1024;
|
|
38
39
|
const WEB_AUTH_COOKIE_NAME = 'manyoyo_web_auth';
|
|
39
40
|
const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
|
|
40
41
|
const WEB_SESSION_KEY_SEPARATOR = '~';
|
|
@@ -165,6 +166,7 @@ function createEmptyWebAgentSession(agentId, agentName) {
|
|
|
165
166
|
agentId,
|
|
166
167
|
agentName: normalizeWebAgentName(agentId, agentName),
|
|
167
168
|
agentPromptCommand: '',
|
|
169
|
+
createdAt: null,
|
|
168
170
|
updatedAt: null,
|
|
169
171
|
messages: [],
|
|
170
172
|
lastResumeAt: null,
|
|
@@ -181,6 +183,7 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
|
|
|
181
183
|
agentPromptCommand: typeof source.agentPromptCommand === 'string'
|
|
182
184
|
? normalizeAgentPromptCommandTemplate(source.agentPromptCommand, `agents.${agentId}.agentPromptCommand`)
|
|
183
185
|
: '',
|
|
186
|
+
createdAt: typeof source.createdAt === 'string' ? source.createdAt : null,
|
|
184
187
|
updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : null,
|
|
185
188
|
messages: Array.isArray(source.messages) ? source.messages : [],
|
|
186
189
|
lastResumeAt: typeof source.lastResumeAt === 'string' ? source.lastResumeAt : null,
|
|
@@ -384,6 +387,45 @@ function listWebAgentSessions(history, options = {}) {
|
|
|
384
387
|
});
|
|
385
388
|
}
|
|
386
389
|
|
|
390
|
+
function getWebAgentCreationRank(agentId) {
|
|
391
|
+
if (agentId === WEB_DEFAULT_AGENT_ID) {
|
|
392
|
+
return 1;
|
|
393
|
+
}
|
|
394
|
+
const matched = String(agentId || '').match(/^agent-(\d+)$/);
|
|
395
|
+
return matched ? (Number(matched[1]) || 0) : 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function getWebSessionCreatedTime(sessionSummary) {
|
|
399
|
+
if (sessionSummary && sessionSummary.createdAt) {
|
|
400
|
+
const time = new Date(sessionSummary.createdAt).getTime();
|
|
401
|
+
if (Number.isFinite(time)) {
|
|
402
|
+
return time;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return 0;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function compareWebSessionCreatedDesc(a, b) {
|
|
409
|
+
const timeA = getWebSessionCreatedTime(a);
|
|
410
|
+
const timeB = getWebSessionCreatedTime(b);
|
|
411
|
+
if (timeA !== timeB) {
|
|
412
|
+
return timeB - timeA;
|
|
413
|
+
}
|
|
414
|
+
if (a && b && a.containerName === b.containerName) {
|
|
415
|
+
const rankA = getWebAgentCreationRank(a.agentId);
|
|
416
|
+
const rankB = getWebAgentCreationRank(b.agentId);
|
|
417
|
+
if (rankA !== rankB) {
|
|
418
|
+
return rankB - rankA;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const updatedA = a && a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
422
|
+
const updatedB = b && b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
423
|
+
if (updatedA !== updatedB) {
|
|
424
|
+
return updatedB - updatedA;
|
|
425
|
+
}
|
|
426
|
+
return String((a && a.name) || '').localeCompare(String((b && b.name) || ''), 'zh-CN');
|
|
427
|
+
}
|
|
428
|
+
|
|
387
429
|
function createWebSessionMessageId() {
|
|
388
430
|
return `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
|
389
431
|
}
|
|
@@ -402,6 +444,9 @@ function appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, role,
|
|
|
402
444
|
timestamp,
|
|
403
445
|
...extra
|
|
404
446
|
};
|
|
447
|
+
if (!agentSession.createdAt) {
|
|
448
|
+
agentSession.createdAt = timestamp;
|
|
449
|
+
}
|
|
405
450
|
agentSession.messages.push(message);
|
|
406
451
|
|
|
407
452
|
if (agentSession.messages.length > WEB_HISTORY_MAX_MESSAGES) {
|
|
@@ -584,7 +629,11 @@ function createWebAgentSession(history) {
|
|
|
584
629
|
}
|
|
585
630
|
const agentId = `agent-${agentIndex}`;
|
|
586
631
|
const agentSession = createEmptyWebAgentSession(agentId, `AGENT ${agentIndex}`);
|
|
632
|
+
const timestamp = new Date().toISOString();
|
|
633
|
+
agentSession.createdAt = timestamp;
|
|
634
|
+
agentSession.updatedAt = timestamp;
|
|
587
635
|
sessionHistory.agents[agentId] = agentSession;
|
|
636
|
+
sessionHistory.updatedAt = timestamp;
|
|
588
637
|
return agentSession;
|
|
589
638
|
}
|
|
590
639
|
|
|
@@ -2609,7 +2658,12 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
2609
2658
|
const extractedAgentMessage = extractAgentMessageFromStructuredOutput(agentProgram, clippedStdout);
|
|
2610
2659
|
const cleanOutputSource = extractedAgentMessage || clippedRaw;
|
|
2611
2660
|
const output = clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
|
|
2612
|
-
resolve({
|
|
2661
|
+
resolve({
|
|
2662
|
+
exitCode,
|
|
2663
|
+
output,
|
|
2664
|
+
stdout: clippedStdout,
|
|
2665
|
+
stderr: clippedStderr
|
|
2666
|
+
});
|
|
2613
2667
|
});
|
|
2614
2668
|
});
|
|
2615
2669
|
}
|
|
@@ -2631,7 +2685,7 @@ async function execJsonCommandInWebContainer(ctx, containerName, command) {
|
|
|
2631
2685
|
throw new Error(result.output || '容器命令执行失败');
|
|
2632
2686
|
}
|
|
2633
2687
|
try {
|
|
2634
|
-
return JSON.parse(String(result.
|
|
2688
|
+
return JSON.parse(String(result.stdout || '{}'));
|
|
2635
2689
|
} catch (e) {
|
|
2636
2690
|
throw new Error('容器返回了无法解析的 JSON');
|
|
2637
2691
|
}
|
|
@@ -2700,13 +2754,17 @@ try {
|
|
|
2700
2754
|
`);
|
|
2701
2755
|
}
|
|
2702
2756
|
|
|
2703
|
-
function buildContainerFileReadCommand(requestedPath) {
|
|
2757
|
+
function buildContainerFileReadCommand(requestedPath, options = {}) {
|
|
2758
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
2759
|
+
const maxBytes = Number.isFinite(opts.maxBytes) && opts.maxBytes > 0
|
|
2760
|
+
? Math.floor(opts.maxBytes)
|
|
2761
|
+
: 0;
|
|
2704
2762
|
return buildWebContainerNodeCommand(`
|
|
2705
2763
|
// __MANYOYO_FS_READ__
|
|
2706
2764
|
const fs = require('fs');
|
|
2707
2765
|
|
|
2708
2766
|
const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
|
|
2709
|
-
const maxBytes = ${String(
|
|
2767
|
+
const maxBytes = ${String(maxBytes)};
|
|
2710
2768
|
|
|
2711
2769
|
function looksBinary(buffer) {
|
|
2712
2770
|
const length = Math.min(buffer.length, 4096);
|
|
@@ -2731,7 +2789,7 @@ try {
|
|
|
2731
2789
|
}
|
|
2732
2790
|
|
|
2733
2791
|
const size = stat.size;
|
|
2734
|
-
const readBytes = Math.min(size, maxBytes);
|
|
2792
|
+
const readBytes = maxBytes > 0 ? Math.min(size, maxBytes) : size;
|
|
2735
2793
|
const buffer = Buffer.alloc(readBytes);
|
|
2736
2794
|
const fd = fs.openSync(realPath, 'r');
|
|
2737
2795
|
try {
|
|
@@ -2745,14 +2803,14 @@ try {
|
|
|
2745
2803
|
path: realPath,
|
|
2746
2804
|
kind: 'binary',
|
|
2747
2805
|
size,
|
|
2748
|
-
truncated: size > maxBytes
|
|
2806
|
+
truncated: maxBytes > 0 && size > maxBytes
|
|
2749
2807
|
}));
|
|
2750
2808
|
} else {
|
|
2751
2809
|
process.stdout.write(JSON.stringify({
|
|
2752
2810
|
path: realPath,
|
|
2753
2811
|
kind: 'text',
|
|
2754
2812
|
size,
|
|
2755
|
-
truncated: size > maxBytes,
|
|
2813
|
+
truncated: maxBytes > 0 && size > maxBytes,
|
|
2756
2814
|
content: buffer.toString('utf8')
|
|
2757
2815
|
}));
|
|
2758
2816
|
}
|
|
@@ -2764,6 +2822,71 @@ try {
|
|
|
2764
2822
|
`);
|
|
2765
2823
|
}
|
|
2766
2824
|
|
|
2825
|
+
function buildContainerFileWriteCommand(requestedPath, content) {
|
|
2826
|
+
return buildWebContainerNodeCommand(`
|
|
2827
|
+
// __MANYOYO_FS_WRITE__
|
|
2828
|
+
const fs = require('fs');
|
|
2829
|
+
|
|
2830
|
+
const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
|
|
2831
|
+
const nextContent = ${JSON.stringify(String(content == null ? '' : content))};
|
|
2832
|
+
|
|
2833
|
+
try {
|
|
2834
|
+
const realPath = fs.realpathSync(requestedPath);
|
|
2835
|
+
const stat = fs.statSync(realPath);
|
|
2836
|
+
if (!stat.isFile()) {
|
|
2837
|
+
throw new Error('目标不是文件: ' + realPath);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
fs.writeFileSync(realPath, nextContent, 'utf8');
|
|
2841
|
+
const savedStat = fs.statSync(realPath);
|
|
2842
|
+
process.stdout.write(JSON.stringify({
|
|
2843
|
+
path: realPath,
|
|
2844
|
+
saved: true,
|
|
2845
|
+
size: savedStat.size
|
|
2846
|
+
}));
|
|
2847
|
+
} catch (e) {
|
|
2848
|
+
process.stdout.write(JSON.stringify({
|
|
2849
|
+
error: e && e.message ? e.message : '保存文件失败'
|
|
2850
|
+
}));
|
|
2851
|
+
}
|
|
2852
|
+
`);
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
function buildContainerFileMkdirCommand(requestedPath) {
|
|
2856
|
+
return buildWebContainerNodeCommand(`
|
|
2857
|
+
// __MANYOYO_FS_MKDIR__
|
|
2858
|
+
const fs = require('fs');
|
|
2859
|
+
const path = require('path');
|
|
2860
|
+
|
|
2861
|
+
const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
|
|
2862
|
+
|
|
2863
|
+
try {
|
|
2864
|
+
const resolvedPath = path.resolve(requestedPath);
|
|
2865
|
+
const parentPath = path.dirname(resolvedPath);
|
|
2866
|
+
const realParentPath = fs.realpathSync(parentPath);
|
|
2867
|
+
const targetPath = path.join(realParentPath, path.basename(resolvedPath));
|
|
2868
|
+
if (fs.existsSync(targetPath)) {
|
|
2869
|
+
throw new Error('目录已存在: ' + targetPath);
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
2873
|
+
const stat = fs.statSync(targetPath);
|
|
2874
|
+
process.stdout.write(JSON.stringify({
|
|
2875
|
+
path: targetPath,
|
|
2876
|
+
name: path.basename(targetPath),
|
|
2877
|
+
kind: 'directory',
|
|
2878
|
+
size: 0,
|
|
2879
|
+
mtimeMs: stat.mtimeMs,
|
|
2880
|
+
created: true
|
|
2881
|
+
}));
|
|
2882
|
+
} catch (e) {
|
|
2883
|
+
process.stdout.write(JSON.stringify({
|
|
2884
|
+
error: e && e.message ? e.message : '创建目录失败'
|
|
2885
|
+
}));
|
|
2886
|
+
}
|
|
2887
|
+
`);
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2767
2890
|
async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
|
|
2768
2891
|
const opts = options && typeof options === 'object' ? options : {};
|
|
2769
2892
|
const sessionRef = typeof sessionRefOrContainerName === 'string'
|
|
@@ -3041,7 +3164,13 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
|
|
|
3041
3164
|
status: containerInfo.status || 'history',
|
|
3042
3165
|
defaultCommand: containerInfo.defaultCommand || ''
|
|
3043
3166
|
});
|
|
3044
|
-
const
|
|
3167
|
+
const createdAt = agentSession.createdAt || containerInfo.createdAt || null;
|
|
3168
|
+
const updatedAt = agentSession.updatedAt
|
|
3169
|
+
|| (latestMessage && latestMessage.timestamp)
|
|
3170
|
+
|| (agentId === WEB_DEFAULT_AGENT_ID
|
|
3171
|
+
? (history.updatedAt || containerInfo.createdAt)
|
|
3172
|
+
: null)
|
|
3173
|
+
|| null;
|
|
3045
3174
|
return {
|
|
3046
3175
|
name: buildWebSessionKey(containerName, agentId),
|
|
3047
3176
|
containerName,
|
|
@@ -3049,6 +3178,7 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
|
|
|
3049
3178
|
agentName: agentSession.agentName,
|
|
3050
3179
|
status: containerInfo.status || 'history',
|
|
3051
3180
|
image: containerInfo.image || '',
|
|
3181
|
+
createdAt,
|
|
3052
3182
|
updatedAt,
|
|
3053
3183
|
messageCount: agentSession.messages.length,
|
|
3054
3184
|
agentEnabled: isAgentPromptCommandEnabled(effectiveAgentPromptCommand),
|
|
@@ -3533,6 +3663,25 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3533
3663
|
});
|
|
3534
3664
|
}
|
|
3535
3665
|
},
|
|
3666
|
+
{
|
|
3667
|
+
method: 'POST',
|
|
3668
|
+
match: currentPath => currentPath === '/api/fs/directories/mkdir' ? [] : null,
|
|
3669
|
+
handler: async () => {
|
|
3670
|
+
const payload = await readJsonBody(req);
|
|
3671
|
+
const requestedPath = expandHomeAliasPath(String(payload && payload.path ? payload.path : '').trim());
|
|
3672
|
+
if (!requestedPath) {
|
|
3673
|
+
sendJson(res, 400, { error: 'path 不能为空' });
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
const targetPath = path.resolve(requestedPath);
|
|
3678
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
3679
|
+
sendJson(res, 200, {
|
|
3680
|
+
path: targetPath,
|
|
3681
|
+
created: true
|
|
3682
|
+
});
|
|
3683
|
+
}
|
|
3684
|
+
},
|
|
3536
3685
|
{
|
|
3537
3686
|
method: 'GET',
|
|
3538
3687
|
match: currentPath => currentPath === '/api/config' ? [] : null,
|
|
@@ -3594,11 +3743,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3594
3743
|
}))
|
|
3595
3744
|
.filter(Boolean);
|
|
3596
3745
|
})
|
|
3597
|
-
.sort(
|
|
3598
|
-
const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
3599
|
-
const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
3600
|
-
return timeB - timeA;
|
|
3601
|
-
});
|
|
3746
|
+
.sort(compareWebSessionCreatedDesc);
|
|
3602
3747
|
|
|
3603
3748
|
sendJson(res, 200, { sessions });
|
|
3604
3749
|
}
|
|
@@ -3696,6 +3841,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3696
3841
|
}
|
|
3697
3842
|
const requestUrl = new URL(req.url || '/api/sessions/x/fs/read', 'http://localhost');
|
|
3698
3843
|
const targetPath = String(requestUrl.searchParams.get('path') || '').trim();
|
|
3844
|
+
const fullRequested = ['1', 'true', 'yes'].includes(String(requestUrl.searchParams.get('full') || '').toLowerCase());
|
|
3699
3845
|
if (!targetPath) {
|
|
3700
3846
|
sendJson(res, 400, { error: 'path 不能为空' });
|
|
3701
3847
|
return;
|
|
@@ -3705,7 +3851,9 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3705
3851
|
const payload = await execJsonCommandInWebContainer(
|
|
3706
3852
|
ctx,
|
|
3707
3853
|
sessionRef.containerName,
|
|
3708
|
-
buildContainerFileReadCommand(targetPath
|
|
3854
|
+
buildContainerFileReadCommand(targetPath, {
|
|
3855
|
+
maxBytes: fullRequested ? 0 : WEB_FILE_PREVIEW_MAX_BYTES
|
|
3856
|
+
})
|
|
3709
3857
|
);
|
|
3710
3858
|
if (payload && payload.error) {
|
|
3711
3859
|
sendJson(res, 400, { error: payload.error });
|
|
@@ -3713,10 +3861,77 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3713
3861
|
}
|
|
3714
3862
|
if (payload && payload.kind === 'text') {
|
|
3715
3863
|
payload.language = inferFileLanguage(payload.path);
|
|
3864
|
+
payload.editable = payload.truncated !== true
|
|
3865
|
+
&& Number(payload.size || 0) < WEB_FILE_EDIT_MAX_BYTES;
|
|
3716
3866
|
}
|
|
3717
3867
|
sendJson(res, 200, payload);
|
|
3718
3868
|
}
|
|
3719
3869
|
},
|
|
3870
|
+
{
|
|
3871
|
+
method: 'PUT',
|
|
3872
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/write$/),
|
|
3873
|
+
handler: async match => {
|
|
3874
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3875
|
+
if (!sessionRef) {
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3878
|
+
const payload = await readJsonBody(req);
|
|
3879
|
+
const targetPath = String(payload && payload.path ? payload.path : '').trim();
|
|
3880
|
+
const content = typeof payload.content === 'string' ? payload.content : null;
|
|
3881
|
+
if (!targetPath) {
|
|
3882
|
+
sendJson(res, 400, { error: 'path 不能为空' });
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
if (content === null) {
|
|
3886
|
+
sendJson(res, 400, { error: 'content 必须是字符串' });
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
if (Buffer.byteLength(content, 'utf8') >= WEB_FILE_EDIT_MAX_BYTES) {
|
|
3890
|
+
sendJson(res, 400, { error: '文件过大,当前仅支持编辑小于 2MB 的文本文件' });
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
3895
|
+
const result = await execJsonCommandInWebContainer(
|
|
3896
|
+
ctx,
|
|
3897
|
+
sessionRef.containerName,
|
|
3898
|
+
buildContainerFileWriteCommand(targetPath, content)
|
|
3899
|
+
);
|
|
3900
|
+
if (result && result.error) {
|
|
3901
|
+
sendJson(res, 400, { error: result.error });
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
3904
|
+
sendJson(res, 200, result);
|
|
3905
|
+
}
|
|
3906
|
+
},
|
|
3907
|
+
{
|
|
3908
|
+
method: 'POST',
|
|
3909
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/mkdir$/),
|
|
3910
|
+
handler: async match => {
|
|
3911
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3912
|
+
if (!sessionRef) {
|
|
3913
|
+
return;
|
|
3914
|
+
}
|
|
3915
|
+
const payload = await readJsonBody(req);
|
|
3916
|
+
const targetPath = String(payload && payload.path ? payload.path : '').trim();
|
|
3917
|
+
if (!targetPath) {
|
|
3918
|
+
sendJson(res, 400, { error: 'path 不能为空' });
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
3923
|
+
const result = await execJsonCommandInWebContainer(
|
|
3924
|
+
ctx,
|
|
3925
|
+
sessionRef.containerName,
|
|
3926
|
+
buildContainerFileMkdirCommand(targetPath)
|
|
3927
|
+
);
|
|
3928
|
+
if (result && result.error) {
|
|
3929
|
+
sendJson(res, 400, { error: result.error });
|
|
3930
|
+
return;
|
|
3931
|
+
}
|
|
3932
|
+
sendJson(res, 200, result);
|
|
3933
|
+
}
|
|
3934
|
+
},
|
|
3720
3935
|
{
|
|
3721
3936
|
method: 'GET',
|
|
3722
3937
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
|