@nextclaw/server 0.6.12 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2260 -2116
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -10,8 +10,80 @@ import { join } from "path";
|
|
|
10
10
|
|
|
11
11
|
// src/ui/router.ts
|
|
12
12
|
import { Hono } from "hono";
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
|
|
14
|
+
// src/ui/router/response.ts
|
|
15
|
+
function ok(data) {
|
|
16
|
+
return { ok: true, data };
|
|
17
|
+
}
|
|
18
|
+
function err(code, message, details) {
|
|
19
|
+
return { ok: false, error: { code, message, details } };
|
|
20
|
+
}
|
|
21
|
+
async function readJson(req) {
|
|
22
|
+
try {
|
|
23
|
+
const data = await req.json();
|
|
24
|
+
return { ok: true, data };
|
|
25
|
+
} catch {
|
|
26
|
+
return { ok: false };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function isRecord(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
function readErrorMessage(value, fallback) {
|
|
33
|
+
if (!isRecord(value)) {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
const maybeError = value.error;
|
|
37
|
+
if (!isRecord(maybeError)) {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
return typeof maybeError.message === "string" && maybeError.message.trim().length > 0 ? maybeError.message : fallback;
|
|
41
|
+
}
|
|
42
|
+
function readNonEmptyString(value) {
|
|
43
|
+
if (typeof value !== "string") {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
const trimmed = value.trim();
|
|
47
|
+
return trimmed || void 0;
|
|
48
|
+
}
|
|
49
|
+
function formatUserFacingError(error, maxChars = 320) {
|
|
50
|
+
const raw = error instanceof Error ? error.message || error.name || "Unknown error" : String(error ?? "Unknown error");
|
|
51
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
52
|
+
if (!normalized) {
|
|
53
|
+
return "Unknown error";
|
|
54
|
+
}
|
|
55
|
+
if (normalized.length <= maxChars) {
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/ui/router/app.controller.ts
|
|
62
|
+
function buildAppMetaView(options) {
|
|
63
|
+
const productVersion = options.productVersion?.trim();
|
|
64
|
+
return {
|
|
65
|
+
name: "NextClaw",
|
|
66
|
+
productVersion: productVersion && productVersion.length > 0 ? productVersion : "0.0.0"
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var AppRoutesController = class {
|
|
70
|
+
constructor(options) {
|
|
71
|
+
this.options = options;
|
|
72
|
+
}
|
|
73
|
+
health = (c) => c.json(
|
|
74
|
+
ok({
|
|
75
|
+
status: "ok",
|
|
76
|
+
services: {
|
|
77
|
+
chatRuntime: this.options.chatRuntime ? "ready" : "unavailable",
|
|
78
|
+
cronService: this.options.cronService ? "ready" : "unavailable"
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
appMeta = (c) => c.json(ok(buildAppMetaView(this.options)));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/ui/router/chat.controller.ts
|
|
86
|
+
import * as NextclawCore2 from "@nextclaw/core";
|
|
15
87
|
|
|
16
88
|
// src/ui/config.ts
|
|
17
89
|
import {
|
|
@@ -1329,569 +1401,1761 @@ function updateSecrets(configPath, patch) {
|
|
|
1329
1401
|
};
|
|
1330
1402
|
}
|
|
1331
1403
|
|
|
1332
|
-
// src/ui/
|
|
1333
|
-
import
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
import { isAbsolute, resolve } from "path";
|
|
1337
|
-
import {
|
|
1338
|
-
ConfigSchema as ConfigSchema2,
|
|
1339
|
-
loadConfig as loadConfig2,
|
|
1340
|
-
saveConfig as saveConfig2
|
|
1341
|
-
} from "@nextclaw/core";
|
|
1342
|
-
var authSessions = /* @__PURE__ */ new Map();
|
|
1343
|
-
var DEFAULT_AUTH_INTERVAL_MS = 2e3;
|
|
1344
|
-
var MAX_AUTH_INTERVAL_MS = 1e4;
|
|
1345
|
-
function normalizePositiveInt(value, fallback) {
|
|
1346
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
1347
|
-
return fallback;
|
|
1348
|
-
}
|
|
1349
|
-
return Math.floor(value);
|
|
1404
|
+
// src/ui/router/chat-utils.ts
|
|
1405
|
+
import * as NextclawCore from "@nextclaw/core";
|
|
1406
|
+
function normalizeSessionType2(value) {
|
|
1407
|
+
return readNonEmptyString(value)?.toLowerCase();
|
|
1350
1408
|
}
|
|
1351
|
-
function
|
|
1352
|
-
if (
|
|
1353
|
-
return
|
|
1409
|
+
function resolveSessionTypeLabel(sessionType) {
|
|
1410
|
+
if (sessionType === "native") {
|
|
1411
|
+
return "Native";
|
|
1354
1412
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
function toBase64Url(buffer) {
|
|
1358
|
-
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
1359
|
-
}
|
|
1360
|
-
function buildPkce() {
|
|
1361
|
-
const verifier = toBase64Url(randomBytes(48));
|
|
1362
|
-
const challenge = toBase64Url(createHash("sha256").update(verifier).digest());
|
|
1363
|
-
return { verifier, challenge };
|
|
1364
|
-
}
|
|
1365
|
-
function withTrailingSlash(value) {
|
|
1366
|
-
return value.endsWith("/") ? value : `${value}/`;
|
|
1367
|
-
}
|
|
1368
|
-
function cleanupExpiredAuthSessions(now = Date.now()) {
|
|
1369
|
-
for (const [sessionId, session] of authSessions.entries()) {
|
|
1370
|
-
if (session.expiresAtMs <= now) {
|
|
1371
|
-
authSessions.delete(sessionId);
|
|
1372
|
-
}
|
|
1413
|
+
if (sessionType === "codex-sdk") {
|
|
1414
|
+
return "Codex";
|
|
1373
1415
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
const deviceCodeEndpoint = new URL(deviceCodePath, withTrailingSlash(baseUrl)).toString();
|
|
1377
|
-
const tokenEndpoint = new URL(tokenPath, withTrailingSlash(baseUrl)).toString();
|
|
1378
|
-
return { deviceCodeEndpoint, tokenEndpoint };
|
|
1379
|
-
}
|
|
1380
|
-
function resolveAuthNote(params) {
|
|
1381
|
-
return params.zh ?? params.en;
|
|
1382
|
-
}
|
|
1383
|
-
function resolveLocalizedMethodLabel(method, fallbackId) {
|
|
1384
|
-
return method.label?.zh ?? method.label?.en ?? fallbackId;
|
|
1385
|
-
}
|
|
1386
|
-
function resolveLocalizedMethodHint(method) {
|
|
1387
|
-
return method.hint?.zh ?? method.hint?.en;
|
|
1388
|
-
}
|
|
1389
|
-
function normalizeMethodId(value) {
|
|
1390
|
-
if (typeof value !== "string") {
|
|
1391
|
-
return void 0;
|
|
1416
|
+
if (sessionType === "claude-agent-sdk") {
|
|
1417
|
+
return "Claude Code";
|
|
1392
1418
|
}
|
|
1393
|
-
|
|
1394
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
1419
|
+
return sessionType;
|
|
1395
1420
|
}
|
|
1396
|
-
function
|
|
1397
|
-
|
|
1398
|
-
const methods = (auth.methods ?? []).filter((entry) => normalizeMethodId(entry.id));
|
|
1399
|
-
const cleanRequestedMethodId = normalizeMethodId(requestedMethodId);
|
|
1400
|
-
if (methods.length === 0) {
|
|
1401
|
-
if (cleanRequestedMethodId) {
|
|
1402
|
-
throw new Error(`provider auth method is not supported: ${cleanRequestedMethodId}`);
|
|
1403
|
-
}
|
|
1421
|
+
async function buildChatSessionTypesView(chatRuntime) {
|
|
1422
|
+
if (!chatRuntime?.listSessionTypes) {
|
|
1404
1423
|
return {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
deviceCodePath: auth.deviceCodePath,
|
|
1408
|
-
tokenPath: auth.tokenPath,
|
|
1409
|
-
clientId: auth.clientId,
|
|
1410
|
-
scope: auth.scope,
|
|
1411
|
-
grantType: auth.grantType,
|
|
1412
|
-
usePkce: Boolean(auth.usePkce)
|
|
1424
|
+
defaultType: DEFAULT_SESSION_TYPE,
|
|
1425
|
+
options: [{ value: DEFAULT_SESSION_TYPE, label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE) }]
|
|
1413
1426
|
};
|
|
1414
1427
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1428
|
+
const payload = await chatRuntime.listSessionTypes();
|
|
1429
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
1430
|
+
for (const rawOption of payload.options ?? []) {
|
|
1431
|
+
const normalized = normalizeSessionType2(rawOption.value);
|
|
1432
|
+
if (!normalized) {
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
deduped.set(normalized, {
|
|
1436
|
+
value: normalized,
|
|
1437
|
+
label: readNonEmptyString(rawOption.label) ?? resolveSessionTypeLabel(normalized)
|
|
1438
|
+
});
|
|
1419
1439
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1440
|
+
if (!deduped.has(DEFAULT_SESSION_TYPE)) {
|
|
1441
|
+
deduped.set(DEFAULT_SESSION_TYPE, {
|
|
1442
|
+
value: DEFAULT_SESSION_TYPE,
|
|
1443
|
+
label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE)
|
|
1444
|
+
});
|
|
1423
1445
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1446
|
+
const defaultType = normalizeSessionType2(payload.defaultType) ?? DEFAULT_SESSION_TYPE;
|
|
1447
|
+
if (!deduped.has(defaultType)) {
|
|
1448
|
+
deduped.set(defaultType, {
|
|
1449
|
+
value: defaultType,
|
|
1450
|
+
label: resolveSessionTypeLabel(defaultType)
|
|
1451
|
+
});
|
|
1426
1452
|
}
|
|
1453
|
+
const options = Array.from(deduped.values()).sort((left, right) => {
|
|
1454
|
+
if (left.value === DEFAULT_SESSION_TYPE) {
|
|
1455
|
+
return -1;
|
|
1456
|
+
}
|
|
1457
|
+
if (right.value === DEFAULT_SESSION_TYPE) {
|
|
1458
|
+
return 1;
|
|
1459
|
+
}
|
|
1460
|
+
return left.value.localeCompare(right.value);
|
|
1461
|
+
});
|
|
1427
1462
|
return {
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
baseUrl: selectedMethod.baseUrl ?? auth.baseUrl,
|
|
1431
|
-
deviceCodePath: selectedMethod.deviceCodePath ?? auth.deviceCodePath,
|
|
1432
|
-
tokenPath: selectedMethod.tokenPath ?? auth.tokenPath,
|
|
1433
|
-
clientId: selectedMethod.clientId ?? auth.clientId,
|
|
1434
|
-
scope: selectedMethod.scope ?? auth.scope,
|
|
1435
|
-
grantType: selectedMethod.grantType ?? auth.grantType,
|
|
1436
|
-
usePkce: selectedMethod.usePkce ?? Boolean(auth.usePkce),
|
|
1437
|
-
defaultApiBase: selectedMethod.defaultApiBase
|
|
1463
|
+
defaultType,
|
|
1464
|
+
options
|
|
1438
1465
|
};
|
|
1439
1466
|
}
|
|
1440
|
-
function
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
}
|
|
1445
|
-
if (normalized >= 1e12) {
|
|
1446
|
-
return Math.floor(normalized);
|
|
1447
|
-
}
|
|
1448
|
-
if (normalized >= 1e9) {
|
|
1449
|
-
return Math.floor(normalized * 1e3);
|
|
1450
|
-
}
|
|
1451
|
-
return Date.now() + Math.floor(normalized * 1e3);
|
|
1467
|
+
function resolveAgentIdFromSessionKey(sessionKey) {
|
|
1468
|
+
const parsed = NextclawCore.parseAgentScopedSessionKey(sessionKey);
|
|
1469
|
+
const agentId = readNonEmptyString(parsed?.agentId);
|
|
1470
|
+
return agentId;
|
|
1452
1471
|
}
|
|
1453
|
-
function
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
}
|
|
1458
|
-
if (normalized <= 30) {
|
|
1459
|
-
return Math.floor(normalized * 1e3);
|
|
1460
|
-
}
|
|
1461
|
-
return Math.floor(normalized);
|
|
1472
|
+
function createChatRunId() {
|
|
1473
|
+
const now = Date.now().toString(36);
|
|
1474
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
1475
|
+
return `run-${now}-${rand}`;
|
|
1462
1476
|
}
|
|
1463
|
-
function
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
return record.error_description.trim();
|
|
1470
|
-
}
|
|
1471
|
-
if (typeof record.error === "string" && record.error.trim()) {
|
|
1472
|
-
return record.error.trim();
|
|
1477
|
+
function isChatRunState(value) {
|
|
1478
|
+
return value === "queued" || value === "running" || value === "completed" || value === "failed" || value === "aborted";
|
|
1479
|
+
}
|
|
1480
|
+
function readChatRunStates(value) {
|
|
1481
|
+
if (typeof value !== "string") {
|
|
1482
|
+
return void 0;
|
|
1473
1483
|
}
|
|
1474
|
-
const
|
|
1475
|
-
if (
|
|
1476
|
-
return
|
|
1484
|
+
const values = value.split(",").map((item) => item.trim().toLowerCase()).filter((item) => Boolean(item) && isChatRunState(item));
|
|
1485
|
+
if (values.length === 0) {
|
|
1486
|
+
return void 0;
|
|
1477
1487
|
}
|
|
1478
|
-
return
|
|
1488
|
+
return Array.from(new Set(values));
|
|
1479
1489
|
}
|
|
1480
|
-
function
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1490
|
+
function buildChatTurnView(params) {
|
|
1491
|
+
const completedAt = /* @__PURE__ */ new Date();
|
|
1492
|
+
return {
|
|
1493
|
+
reply: String(params.result.reply ?? ""),
|
|
1494
|
+
sessionKey: readNonEmptyString(params.result.sessionKey) ?? params.fallbackSessionKey,
|
|
1495
|
+
...readNonEmptyString(params.result.agentId) || params.requestedAgentId ? { agentId: readNonEmptyString(params.result.agentId) ?? params.requestedAgentId } : {},
|
|
1496
|
+
...readNonEmptyString(params.result.model) || params.requestedModel ? { model: readNonEmptyString(params.result.model) ?? params.requestedModel } : {},
|
|
1497
|
+
requestedAt: params.requestedAt.toISOString(),
|
|
1498
|
+
completedAt: completedAt.toISOString(),
|
|
1499
|
+
durationMs: Math.max(0, completedAt.getTime() - params.startedAtMs)
|
|
1500
|
+
};
|
|
1489
1501
|
}
|
|
1490
|
-
function
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
1504
|
-
return resolve(trimmed);
|
|
1502
|
+
function buildChatTurnViewFromRun(params) {
|
|
1503
|
+
const requestedAt = readNonEmptyString(params.run.requestedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1504
|
+
const completedAt = readNonEmptyString(params.run.completedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1505
|
+
const requestedAtMs = Date.parse(requestedAt);
|
|
1506
|
+
const completedAtMs = Date.parse(completedAt);
|
|
1507
|
+
return {
|
|
1508
|
+
reply: readNonEmptyString(params.run.reply) ?? params.fallbackReply ?? "",
|
|
1509
|
+
sessionKey: readNonEmptyString(params.run.sessionKey) ?? params.fallbackSessionKey,
|
|
1510
|
+
...readNonEmptyString(params.run.agentId) || params.fallbackAgentId ? { agentId: readNonEmptyString(params.run.agentId) ?? params.fallbackAgentId } : {},
|
|
1511
|
+
...readNonEmptyString(params.run.model) || params.fallbackModel ? { model: readNonEmptyString(params.run.model) ?? params.fallbackModel } : {},
|
|
1512
|
+
requestedAt,
|
|
1513
|
+
completedAt,
|
|
1514
|
+
durationMs: Number.isFinite(requestedAtMs) && Number.isFinite(completedAtMs) ? Math.max(0, completedAtMs - requestedAtMs) : 0
|
|
1515
|
+
};
|
|
1505
1516
|
}
|
|
1506
|
-
function
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
const asNumber = Number(value);
|
|
1512
|
-
if (Number.isFinite(asNumber) && asNumber > 0) {
|
|
1513
|
-
return Math.floor(asNumber);
|
|
1514
|
-
}
|
|
1515
|
-
const parsedTime = Date.parse(value);
|
|
1516
|
-
if (Number.isFinite(parsedTime) && parsedTime > 0) {
|
|
1517
|
-
return parsedTime;
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
return null;
|
|
1517
|
+
function toSseFrame(event, data) {
|
|
1518
|
+
return `event: ${event}
|
|
1519
|
+
data: ${JSON.stringify(data)}
|
|
1520
|
+
|
|
1521
|
+
`;
|
|
1521
1522
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
if (typeof rawValue !== "string") {
|
|
1528
|
-
return null;
|
|
1523
|
+
|
|
1524
|
+
// src/ui/router/chat.controller.ts
|
|
1525
|
+
var ChatRoutesController = class {
|
|
1526
|
+
constructor(options) {
|
|
1527
|
+
this.options = options;
|
|
1529
1528
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
apiKey: "",
|
|
1540
|
-
apiBase: null,
|
|
1541
|
-
extraHeaders: null,
|
|
1542
|
-
wireApi: "auto",
|
|
1543
|
-
models: [],
|
|
1544
|
-
modelThinking: {}
|
|
1529
|
+
getCapabilities = async (c) => {
|
|
1530
|
+
const chatRuntime = this.options.chatRuntime;
|
|
1531
|
+
if (!chatRuntime) {
|
|
1532
|
+
return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
|
|
1533
|
+
}
|
|
1534
|
+
const query = c.req.query();
|
|
1535
|
+
const params = {
|
|
1536
|
+
sessionKey: readNonEmptyString(query.sessionKey),
|
|
1537
|
+
agentId: readNonEmptyString(query.agentId)
|
|
1545
1538
|
};
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
}
|
|
1552
|
-
const next = ConfigSchema2.parse(config);
|
|
1553
|
-
saveConfig2(next, params.configPath);
|
|
1554
|
-
}
|
|
1555
|
-
async function startProviderAuth(configPath, providerName, options) {
|
|
1556
|
-
cleanupExpiredAuthSessions();
|
|
1557
|
-
const spec = findServerBuiltinProviderByName(providerName);
|
|
1558
|
-
if (!spec?.auth || spec.auth.kind !== "device_code") {
|
|
1559
|
-
return null;
|
|
1560
|
-
}
|
|
1561
|
-
const resolvedMethod = resolveAuthMethod(spec.auth, options?.methodId);
|
|
1562
|
-
const { deviceCodeEndpoint, tokenEndpoint } = resolveDeviceCodeEndpoints(
|
|
1563
|
-
resolvedMethod.baseUrl,
|
|
1564
|
-
resolvedMethod.deviceCodePath,
|
|
1565
|
-
resolvedMethod.tokenPath
|
|
1566
|
-
);
|
|
1567
|
-
const pkce = resolvedMethod.usePkce ? buildPkce() : null;
|
|
1568
|
-
let authorizationCode = "";
|
|
1569
|
-
let tokenCodeField = "device_code";
|
|
1570
|
-
let userCode = "";
|
|
1571
|
-
let verificationUri = "";
|
|
1572
|
-
let intervalMs = DEFAULT_AUTH_INTERVAL_MS;
|
|
1573
|
-
let expiresAtMs = Date.now() + 6e5;
|
|
1574
|
-
if (resolvedMethod.protocol === "minimax_user_code") {
|
|
1575
|
-
if (!pkce) {
|
|
1576
|
-
throw new Error("MiniMax OAuth requires PKCE");
|
|
1539
|
+
try {
|
|
1540
|
+
const capabilities = chatRuntime.getCapabilities ? await chatRuntime.getCapabilities(params) : { stopSupported: Boolean(chatRuntime.stopTurn) };
|
|
1541
|
+
return c.json(ok(capabilities));
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
return c.json(err("CHAT_RUNTIME_FAILED", String(error)), 500);
|
|
1577
1544
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
state
|
|
1586
|
-
});
|
|
1587
|
-
const response = await fetch(deviceCodeEndpoint, {
|
|
1588
|
-
method: "POST",
|
|
1589
|
-
headers: {
|
|
1590
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
1591
|
-
Accept: "application/json",
|
|
1592
|
-
"x-request-id": randomUUID()
|
|
1593
|
-
},
|
|
1594
|
-
body
|
|
1595
|
-
});
|
|
1596
|
-
const payload = await response.json().catch(() => ({}));
|
|
1597
|
-
if (!response.ok) {
|
|
1598
|
-
throw new Error(buildMinimaxErrorMessage(payload, response.statusText || "MiniMax OAuth start failed"));
|
|
1545
|
+
};
|
|
1546
|
+
getSessionTypes = async (c) => {
|
|
1547
|
+
try {
|
|
1548
|
+
const payload = await buildChatSessionTypesView(this.options.chatRuntime);
|
|
1549
|
+
return c.json(ok(payload));
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
return c.json(err("CHAT_SESSION_TYPES_FAILED", String(error)), 500);
|
|
1599
1552
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1553
|
+
};
|
|
1554
|
+
getCommands = async (c) => {
|
|
1555
|
+
try {
|
|
1556
|
+
const config = loadConfigOrDefault(this.options.configPath);
|
|
1557
|
+
const registry = new NextclawCore2.CommandRegistry(config);
|
|
1558
|
+
const commands = registry.listSlashCommands().map((command) => ({
|
|
1559
|
+
name: command.name,
|
|
1560
|
+
description: command.description,
|
|
1561
|
+
...Array.isArray(command.options) && command.options.length > 0 ? {
|
|
1562
|
+
options: command.options.map((option) => ({
|
|
1563
|
+
name: option.name,
|
|
1564
|
+
description: option.description,
|
|
1565
|
+
type: option.type,
|
|
1566
|
+
...option.required === true ? { required: true } : {}
|
|
1567
|
+
}))
|
|
1568
|
+
} : {}
|
|
1569
|
+
}));
|
|
1570
|
+
const payload = {
|
|
1571
|
+
commands,
|
|
1572
|
+
total: commands.length
|
|
1573
|
+
};
|
|
1574
|
+
return c.json(ok(payload));
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
return c.json(err("CHAT_COMMANDS_FAILED", String(error)), 500);
|
|
1602
1577
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
throw new Error("provider auth payload is incomplete");
|
|
1578
|
+
};
|
|
1579
|
+
processTurn = async (c) => {
|
|
1580
|
+
if (!this.options.chatRuntime) {
|
|
1581
|
+
return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
|
|
1608
1582
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
} else {
|
|
1613
|
-
const body = new URLSearchParams({
|
|
1614
|
-
client_id: resolvedMethod.clientId,
|
|
1615
|
-
scope: resolvedMethod.scope
|
|
1616
|
-
});
|
|
1617
|
-
if (pkce) {
|
|
1618
|
-
body.set("code_challenge", pkce.challenge);
|
|
1619
|
-
body.set("code_challenge_method", "S256");
|
|
1583
|
+
const body = await readJson(c.req.raw);
|
|
1584
|
+
if (!body.ok) {
|
|
1585
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
1620
1586
|
}
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
1625
|
-
Accept: "application/json"
|
|
1626
|
-
},
|
|
1627
|
-
body
|
|
1628
|
-
});
|
|
1629
|
-
const payload = await response.json().catch(() => ({}));
|
|
1630
|
-
if (!response.ok) {
|
|
1631
|
-
const message = payload.error_description || payload.error || response.statusText || "device code auth failed";
|
|
1632
|
-
throw new Error(message);
|
|
1587
|
+
const message = readNonEmptyString(body.data.message);
|
|
1588
|
+
if (!message) {
|
|
1589
|
+
return c.json(err("INVALID_BODY", "message is required"), 400);
|
|
1633
1590
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1591
|
+
const sessionKey = readNonEmptyString(body.data.sessionKey) ?? `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
|
|
1592
|
+
const requestedAt = /* @__PURE__ */ new Date();
|
|
1593
|
+
const startedAtMs = requestedAt.getTime();
|
|
1594
|
+
const metadata = isRecord(body.data.metadata) ? body.data.metadata : void 0;
|
|
1595
|
+
const requestedAgentId = readNonEmptyString(body.data.agentId) ?? resolveAgentIdFromSessionKey(sessionKey);
|
|
1596
|
+
const requestedModel = readNonEmptyString(body.data.model);
|
|
1597
|
+
const request = {
|
|
1598
|
+
message,
|
|
1599
|
+
sessionKey,
|
|
1600
|
+
channel: readNonEmptyString(body.data.channel) ?? "ui",
|
|
1601
|
+
chatId: readNonEmptyString(body.data.chatId) ?? "web-ui",
|
|
1602
|
+
...requestedAgentId ? { agentId: requestedAgentId } : {},
|
|
1603
|
+
...requestedModel ? { model: requestedModel } : {},
|
|
1604
|
+
...metadata ? { metadata } : {}
|
|
1605
|
+
};
|
|
1606
|
+
try {
|
|
1607
|
+
const result = await this.options.chatRuntime.processTurn(request);
|
|
1608
|
+
const response = buildChatTurnView({
|
|
1609
|
+
result,
|
|
1610
|
+
fallbackSessionKey: sessionKey,
|
|
1611
|
+
requestedAgentId,
|
|
1612
|
+
requestedModel,
|
|
1613
|
+
requestedAt,
|
|
1614
|
+
startedAtMs
|
|
1615
|
+
});
|
|
1616
|
+
this.options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
1617
|
+
return c.json(ok(response));
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
return c.json(err("CHAT_TURN_FAILED", formatUserFacingError(error)), 500);
|
|
1639
1620
|
}
|
|
1640
|
-
intervalMs = normalizePositiveInt(payload.interval, DEFAULT_AUTH_INTERVAL_MS / 1e3) * 1e3;
|
|
1641
|
-
const expiresInSec = normalizePositiveInt(payload.expires_in, 600);
|
|
1642
|
-
expiresAtMs = Date.now() + expiresInSec * 1e3;
|
|
1643
|
-
}
|
|
1644
|
-
const sessionId = randomUUID();
|
|
1645
|
-
authSessions.set(sessionId, {
|
|
1646
|
-
sessionId,
|
|
1647
|
-
provider: providerName,
|
|
1648
|
-
configPath,
|
|
1649
|
-
authorizationCode,
|
|
1650
|
-
tokenCodeField,
|
|
1651
|
-
protocol: resolvedMethod.protocol,
|
|
1652
|
-
methodId: resolvedMethod.id,
|
|
1653
|
-
codeVerifier: pkce?.verifier,
|
|
1654
|
-
tokenEndpoint,
|
|
1655
|
-
clientId: resolvedMethod.clientId,
|
|
1656
|
-
grantType: resolvedMethod.grantType,
|
|
1657
|
-
defaultApiBase: resolvedMethod.defaultApiBase ?? spec.defaultApiBase,
|
|
1658
|
-
expiresAtMs,
|
|
1659
|
-
intervalMs
|
|
1660
|
-
});
|
|
1661
|
-
const methodConfig = (spec.auth.methods ?? []).find((entry) => normalizeMethodId(entry.id) === resolvedMethod.id);
|
|
1662
|
-
const methodLabel = methodConfig ? resolveLocalizedMethodLabel(methodConfig, resolvedMethod.id ?? "") : void 0;
|
|
1663
|
-
const methodHint = methodConfig ? resolveLocalizedMethodHint(methodConfig) : void 0;
|
|
1664
|
-
return {
|
|
1665
|
-
provider: providerName,
|
|
1666
|
-
kind: "device_code",
|
|
1667
|
-
methodId: resolvedMethod.id,
|
|
1668
|
-
sessionId,
|
|
1669
|
-
verificationUri,
|
|
1670
|
-
userCode,
|
|
1671
|
-
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
1672
|
-
intervalMs,
|
|
1673
|
-
note: methodHint ?? methodLabel ?? resolveAuthNote(spec.auth.note ?? {})
|
|
1674
1621
|
};
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1622
|
+
stopTurn = async (c) => {
|
|
1623
|
+
const chatRuntime = this.options.chatRuntime;
|
|
1624
|
+
if (!chatRuntime?.stopTurn) {
|
|
1625
|
+
return c.json(err("NOT_AVAILABLE", "chat turn stop is not supported by runtime"), 503);
|
|
1626
|
+
}
|
|
1627
|
+
const body = await readJson(c.req.raw);
|
|
1628
|
+
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
1629
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
1630
|
+
}
|
|
1631
|
+
const runId = readNonEmptyString(body.data.runId);
|
|
1632
|
+
if (!runId) {
|
|
1633
|
+
return c.json(err("INVALID_BODY", "runId is required"), 400);
|
|
1634
|
+
}
|
|
1635
|
+
const request = {
|
|
1636
|
+
runId,
|
|
1637
|
+
...readNonEmptyString(body.data.sessionKey) ? { sessionKey: readNonEmptyString(body.data.sessionKey) } : {},
|
|
1638
|
+
...readNonEmptyString(body.data.agentId) ? { agentId: readNonEmptyString(body.data.agentId) } : {}
|
|
1688
1639
|
};
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
body.set(session.tokenCodeField, session.authorizationCode);
|
|
1695
|
-
if (session.codeVerifier) {
|
|
1696
|
-
body.set("code_verifier", session.codeVerifier);
|
|
1697
|
-
}
|
|
1698
|
-
const response = await fetch(session.tokenEndpoint, {
|
|
1699
|
-
method: "POST",
|
|
1700
|
-
headers: {
|
|
1701
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
1702
|
-
Accept: "application/json"
|
|
1703
|
-
},
|
|
1704
|
-
body
|
|
1705
|
-
});
|
|
1706
|
-
let accessToken = "";
|
|
1707
|
-
if (session.protocol === "minimax_user_code") {
|
|
1708
|
-
const raw = await response.text();
|
|
1709
|
-
let payload = {};
|
|
1710
|
-
if (raw) {
|
|
1711
|
-
try {
|
|
1712
|
-
payload = JSON.parse(raw);
|
|
1713
|
-
} catch {
|
|
1714
|
-
payload = {};
|
|
1715
|
-
}
|
|
1640
|
+
try {
|
|
1641
|
+
const result = await chatRuntime.stopTurn(request);
|
|
1642
|
+
return c.json(ok(result));
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
return c.json(err("CHAT_TURN_STOP_FAILED", String(error)), 500);
|
|
1716
1645
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
message
|
|
1723
|
-
};
|
|
1646
|
+
};
|
|
1647
|
+
streamTurn = async (c) => {
|
|
1648
|
+
const chatRuntime = this.options.chatRuntime;
|
|
1649
|
+
if (!chatRuntime) {
|
|
1650
|
+
return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
|
|
1724
1651
|
}
|
|
1725
|
-
const
|
|
1726
|
-
if (
|
|
1727
|
-
|
|
1728
|
-
if (!accessToken) {
|
|
1729
|
-
return {
|
|
1730
|
-
provider: params.providerName,
|
|
1731
|
-
status: "error",
|
|
1732
|
-
message: "provider token response missing access token"
|
|
1733
|
-
};
|
|
1734
|
-
}
|
|
1735
|
-
} else if (status === "error") {
|
|
1736
|
-
const message = buildMinimaxErrorMessage(payload, "authorization failed");
|
|
1737
|
-
const classified = classifyMiniMaxErrorStatus(message);
|
|
1738
|
-
if (classified === "denied" || classified === "expired") {
|
|
1739
|
-
authSessions.delete(params.sessionId);
|
|
1740
|
-
}
|
|
1741
|
-
return {
|
|
1742
|
-
provider: params.providerName,
|
|
1743
|
-
status: classified,
|
|
1744
|
-
message
|
|
1745
|
-
};
|
|
1746
|
-
} else {
|
|
1747
|
-
const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
|
|
1748
|
-
session.intervalMs = nextPollMs;
|
|
1749
|
-
authSessions.set(params.sessionId, session);
|
|
1750
|
-
return {
|
|
1751
|
-
provider: params.providerName,
|
|
1752
|
-
status: "pending",
|
|
1753
|
-
nextPollMs
|
|
1754
|
-
};
|
|
1652
|
+
const body = await readJson(c.req.raw);
|
|
1653
|
+
if (!body.ok) {
|
|
1654
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
1755
1655
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1656
|
+
const message = readNonEmptyString(body.data.message);
|
|
1657
|
+
if (!message) {
|
|
1658
|
+
return c.json(err("INVALID_BODY", "message is required"), 400);
|
|
1659
|
+
}
|
|
1660
|
+
const sessionKey = readNonEmptyString(body.data.sessionKey) ?? `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
|
|
1661
|
+
const requestedAt = /* @__PURE__ */ new Date();
|
|
1662
|
+
const startedAtMs = requestedAt.getTime();
|
|
1663
|
+
const metadata = isRecord(body.data.metadata) ? body.data.metadata : void 0;
|
|
1664
|
+
const requestedAgentId = readNonEmptyString(body.data.agentId) ?? resolveAgentIdFromSessionKey(sessionKey);
|
|
1665
|
+
const requestedModel = readNonEmptyString(body.data.model);
|
|
1666
|
+
let runId = createChatRunId();
|
|
1667
|
+
const supportsManagedRuns = Boolean(chatRuntime.startTurnRun && chatRuntime.streamRun);
|
|
1668
|
+
let stopCapabilities = { stopSupported: Boolean(chatRuntime.stopTurn) };
|
|
1669
|
+
if (chatRuntime.getCapabilities) {
|
|
1670
|
+
try {
|
|
1671
|
+
stopCapabilities = await chatRuntime.getCapabilities({
|
|
1672
|
+
sessionKey,
|
|
1673
|
+
...requestedAgentId ? { agentId: requestedAgentId } : {}
|
|
1674
|
+
});
|
|
1675
|
+
} catch {
|
|
1676
|
+
stopCapabilities = {
|
|
1677
|
+
stopSupported: false,
|
|
1678
|
+
stopReason: "failed to resolve runtime stop capability"
|
|
1775
1679
|
};
|
|
1776
1680
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1681
|
+
}
|
|
1682
|
+
const request = {
|
|
1683
|
+
message,
|
|
1684
|
+
sessionKey,
|
|
1685
|
+
channel: readNonEmptyString(body.data.channel) ?? "ui",
|
|
1686
|
+
chatId: readNonEmptyString(body.data.chatId) ?? "web-ui",
|
|
1687
|
+
runId,
|
|
1688
|
+
...requestedAgentId ? { agentId: requestedAgentId } : {},
|
|
1689
|
+
...requestedModel ? { model: requestedModel } : {},
|
|
1690
|
+
...metadata ? { metadata } : {}
|
|
1691
|
+
};
|
|
1692
|
+
let managedRun = null;
|
|
1693
|
+
if (supportsManagedRuns && chatRuntime.startTurnRun) {
|
|
1694
|
+
try {
|
|
1695
|
+
managedRun = await chatRuntime.startTurnRun(request);
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
return c.json(err("CHAT_TURN_FAILED", formatUserFacingError(error)), 500);
|
|
1784
1698
|
}
|
|
1785
|
-
if (
|
|
1786
|
-
|
|
1787
|
-
return {
|
|
1788
|
-
provider: params.providerName,
|
|
1789
|
-
status: "expired",
|
|
1790
|
-
message: payload.error_description || "authorization session expired"
|
|
1791
|
-
};
|
|
1699
|
+
if (readNonEmptyString(managedRun.runId)) {
|
|
1700
|
+
runId = readNonEmptyString(managedRun.runId);
|
|
1792
1701
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
message: payload.error_description || payload.error || response.statusText || "authorization failed"
|
|
1797
|
-
};
|
|
1798
|
-
}
|
|
1799
|
-
accessToken = payload.access_token?.trim() ?? "";
|
|
1800
|
-
if (!accessToken) {
|
|
1801
|
-
return {
|
|
1802
|
-
provider: params.providerName,
|
|
1803
|
-
status: "error",
|
|
1804
|
-
message: "provider token response missing access token"
|
|
1702
|
+
stopCapabilities = {
|
|
1703
|
+
stopSupported: managedRun.stopSupported,
|
|
1704
|
+
...readNonEmptyString(managedRun.stopReason) ? { stopReason: readNonEmptyString(managedRun.stopReason) } : {}
|
|
1805
1705
|
};
|
|
1806
1706
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1707
|
+
const encoder = new TextEncoder();
|
|
1708
|
+
const stream = new ReadableStream({
|
|
1709
|
+
start: async (controller) => {
|
|
1710
|
+
const push = (event, data) => {
|
|
1711
|
+
controller.enqueue(encoder.encode(toSseFrame(event, data)));
|
|
1712
|
+
};
|
|
1713
|
+
try {
|
|
1714
|
+
push("ready", {
|
|
1715
|
+
sessionKey: managedRun?.sessionKey ?? sessionKey,
|
|
1716
|
+
requestedAt: managedRun?.requestedAt ?? requestedAt.toISOString(),
|
|
1717
|
+
runId,
|
|
1718
|
+
stopSupported: stopCapabilities.stopSupported,
|
|
1719
|
+
...readNonEmptyString(stopCapabilities.stopReason) ? { stopReason: readNonEmptyString(stopCapabilities.stopReason) } : {}
|
|
1720
|
+
});
|
|
1721
|
+
if (supportsManagedRuns && chatRuntime.streamRun) {
|
|
1722
|
+
let hasFinal2 = false;
|
|
1723
|
+
for await (const event of chatRuntime.streamRun({ runId })) {
|
|
1724
|
+
const typed = event;
|
|
1725
|
+
if (typed.type === "delta") {
|
|
1726
|
+
if (typed.delta) {
|
|
1727
|
+
push("delta", { delta: typed.delta });
|
|
1728
|
+
}
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
if (typed.type === "session_event") {
|
|
1732
|
+
push("session_event", typed.event);
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
if (typed.type === "final") {
|
|
1736
|
+
const latestRun = chatRuntime.getRun ? await chatRuntime.getRun({ runId }) : null;
|
|
1737
|
+
const response = latestRun ? buildChatTurnViewFromRun({
|
|
1738
|
+
run: latestRun,
|
|
1739
|
+
fallbackSessionKey: sessionKey,
|
|
1740
|
+
fallbackAgentId: requestedAgentId,
|
|
1741
|
+
fallbackModel: requestedModel,
|
|
1742
|
+
fallbackReply: typed.result.reply
|
|
1743
|
+
}) : buildChatTurnView({
|
|
1744
|
+
result: typed.result,
|
|
1745
|
+
fallbackSessionKey: sessionKey,
|
|
1746
|
+
requestedAgentId,
|
|
1747
|
+
requestedModel,
|
|
1748
|
+
requestedAt,
|
|
1749
|
+
startedAtMs
|
|
1750
|
+
});
|
|
1751
|
+
hasFinal2 = true;
|
|
1752
|
+
push("final", response);
|
|
1753
|
+
this.options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
if (typed.type === "error") {
|
|
1757
|
+
push("error", {
|
|
1758
|
+
code: "CHAT_TURN_FAILED",
|
|
1759
|
+
message: formatUserFacingError(typed.error)
|
|
1760
|
+
});
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (!hasFinal2) {
|
|
1765
|
+
push("error", {
|
|
1766
|
+
code: "CHAT_TURN_FAILED",
|
|
1767
|
+
message: "stream ended without a final result"
|
|
1768
|
+
});
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
push("done", { ok: true });
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const streamTurn = chatRuntime.processTurnStream;
|
|
1775
|
+
if (!streamTurn) {
|
|
1776
|
+
const result = await chatRuntime.processTurn(request);
|
|
1777
|
+
const response = buildChatTurnView({
|
|
1778
|
+
result,
|
|
1779
|
+
fallbackSessionKey: sessionKey,
|
|
1780
|
+
requestedAgentId,
|
|
1781
|
+
requestedModel,
|
|
1782
|
+
requestedAt,
|
|
1783
|
+
startedAtMs
|
|
1784
|
+
});
|
|
1785
|
+
push("final", response);
|
|
1786
|
+
this.options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
1787
|
+
push("done", { ok: true });
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
let hasFinal = false;
|
|
1791
|
+
for await (const event of streamTurn(request)) {
|
|
1792
|
+
const typed = event;
|
|
1793
|
+
if (typed.type === "delta") {
|
|
1794
|
+
if (typed.delta) {
|
|
1795
|
+
push("delta", { delta: typed.delta });
|
|
1796
|
+
}
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
if (typed.type === "session_event") {
|
|
1800
|
+
push("session_event", typed.event);
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
if (typed.type === "final") {
|
|
1804
|
+
const response = buildChatTurnView({
|
|
1805
|
+
result: typed.result,
|
|
1806
|
+
fallbackSessionKey: sessionKey,
|
|
1807
|
+
requestedAgentId,
|
|
1808
|
+
requestedModel,
|
|
1809
|
+
requestedAt,
|
|
1810
|
+
startedAtMs
|
|
1811
|
+
});
|
|
1812
|
+
hasFinal = true;
|
|
1813
|
+
push("final", response);
|
|
1814
|
+
this.options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
if (typed.type === "error") {
|
|
1818
|
+
push("error", {
|
|
1819
|
+
code: "CHAT_TURN_FAILED",
|
|
1820
|
+
message: formatUserFacingError(typed.error)
|
|
1821
|
+
});
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (!hasFinal) {
|
|
1826
|
+
push("error", {
|
|
1827
|
+
code: "CHAT_TURN_FAILED",
|
|
1828
|
+
message: "stream ended without a final result"
|
|
1829
|
+
});
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
push("done", { ok: true });
|
|
1833
|
+
} catch (error) {
|
|
1834
|
+
push("error", {
|
|
1835
|
+
code: "CHAT_TURN_FAILED",
|
|
1836
|
+
message: formatUserFacingError(error)
|
|
1837
|
+
});
|
|
1838
|
+
} finally {
|
|
1839
|
+
controller.close();
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
return new Response(stream, {
|
|
1844
|
+
status: 200,
|
|
1845
|
+
headers: {
|
|
1846
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
1847
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1848
|
+
"Connection": "keep-alive",
|
|
1849
|
+
"X-Accel-Buffering": "no"
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
};
|
|
1853
|
+
listRuns = async (c) => {
|
|
1854
|
+
const chatRuntime = this.options.chatRuntime;
|
|
1855
|
+
if (!chatRuntime?.listRuns) {
|
|
1856
|
+
return c.json(err("NOT_AVAILABLE", "chat run management unavailable"), 503);
|
|
1857
|
+
}
|
|
1858
|
+
const query = c.req.query();
|
|
1859
|
+
const sessionKey = readNonEmptyString(query.sessionKey);
|
|
1860
|
+
const states = readChatRunStates(query.states);
|
|
1861
|
+
const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
|
|
1862
|
+
try {
|
|
1863
|
+
const data = await chatRuntime.listRuns({
|
|
1864
|
+
...sessionKey ? { sessionKey } : {},
|
|
1865
|
+
...states ? { states } : {},
|
|
1866
|
+
...Number.isFinite(limit) ? { limit } : {}
|
|
1867
|
+
});
|
|
1868
|
+
return c.json(ok(data));
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
return c.json(err("CHAT_RUN_QUERY_FAILED", String(error)), 500);
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
getRun = async (c) => {
|
|
1874
|
+
const chatRuntime = this.options.chatRuntime;
|
|
1875
|
+
if (!chatRuntime?.getRun) {
|
|
1876
|
+
return c.json(err("NOT_AVAILABLE", "chat run management unavailable"), 503);
|
|
1877
|
+
}
|
|
1878
|
+
const runId = readNonEmptyString(c.req.param("runId"));
|
|
1879
|
+
if (!runId) {
|
|
1880
|
+
return c.json(err("INVALID_PATH", "runId is required"), 400);
|
|
1881
|
+
}
|
|
1882
|
+
try {
|
|
1883
|
+
const run = await chatRuntime.getRun({ runId });
|
|
1884
|
+
if (!run) {
|
|
1885
|
+
return c.json(err("NOT_FOUND", `chat run not found: ${runId}`), 404);
|
|
1886
|
+
}
|
|
1887
|
+
return c.json(ok(run));
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
return c.json(err("CHAT_RUN_QUERY_FAILED", String(error)), 500);
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
streamRun = async (c) => {
|
|
1893
|
+
const chatRuntime = this.options.chatRuntime;
|
|
1894
|
+
const streamRun = chatRuntime?.streamRun;
|
|
1895
|
+
const getRun = chatRuntime?.getRun;
|
|
1896
|
+
if (!streamRun || !getRun) {
|
|
1897
|
+
return c.json(err("NOT_AVAILABLE", "chat run stream unavailable"), 503);
|
|
1898
|
+
}
|
|
1899
|
+
const runId = readNonEmptyString(c.req.param("runId"));
|
|
1900
|
+
if (!runId) {
|
|
1901
|
+
return c.json(err("INVALID_PATH", "runId is required"), 400);
|
|
1902
|
+
}
|
|
1903
|
+
const query = c.req.query();
|
|
1904
|
+
const fromEventIndex = typeof query.fromEventIndex === "string" ? Number.parseInt(query.fromEventIndex, 10) : void 0;
|
|
1905
|
+
const run = await getRun({ runId });
|
|
1906
|
+
if (!run) {
|
|
1907
|
+
return c.json(err("NOT_FOUND", `chat run not found: ${runId}`), 404);
|
|
1908
|
+
}
|
|
1909
|
+
const encoder = new TextEncoder();
|
|
1910
|
+
const stream = new ReadableStream({
|
|
1911
|
+
start: async (controller) => {
|
|
1912
|
+
const push = (event, data) => {
|
|
1913
|
+
controller.enqueue(encoder.encode(toSseFrame(event, data)));
|
|
1914
|
+
};
|
|
1915
|
+
try {
|
|
1916
|
+
push("ready", {
|
|
1917
|
+
sessionKey: run.sessionKey,
|
|
1918
|
+
requestedAt: run.requestedAt,
|
|
1919
|
+
runId: run.runId,
|
|
1920
|
+
stopSupported: run.stopSupported,
|
|
1921
|
+
...readNonEmptyString(run.stopReason) ? { stopReason: readNonEmptyString(run.stopReason) } : {}
|
|
1922
|
+
});
|
|
1923
|
+
let hasFinal = false;
|
|
1924
|
+
for await (const event of streamRun({
|
|
1925
|
+
runId: run.runId,
|
|
1926
|
+
...Number.isFinite(fromEventIndex) ? { fromEventIndex } : {}
|
|
1927
|
+
})) {
|
|
1928
|
+
const typed = event;
|
|
1929
|
+
if (typed.type === "delta") {
|
|
1930
|
+
if (typed.delta) {
|
|
1931
|
+
push("delta", { delta: typed.delta });
|
|
1932
|
+
}
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
if (typed.type === "session_event") {
|
|
1936
|
+
push("session_event", typed.event);
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
if (typed.type === "final") {
|
|
1940
|
+
const latestRun = await getRun({ runId: run.runId });
|
|
1941
|
+
const response = latestRun ? buildChatTurnViewFromRun({
|
|
1942
|
+
run: latestRun,
|
|
1943
|
+
fallbackSessionKey: run.sessionKey,
|
|
1944
|
+
fallbackAgentId: run.agentId,
|
|
1945
|
+
fallbackModel: run.model,
|
|
1946
|
+
fallbackReply: typed.result.reply
|
|
1947
|
+
}) : buildChatTurnView({
|
|
1948
|
+
result: typed.result,
|
|
1949
|
+
fallbackSessionKey: run.sessionKey,
|
|
1950
|
+
requestedAgentId: run.agentId,
|
|
1951
|
+
requestedModel: run.model,
|
|
1952
|
+
requestedAt: new Date(run.requestedAt),
|
|
1953
|
+
startedAtMs: Date.parse(run.requestedAt)
|
|
1954
|
+
});
|
|
1955
|
+
hasFinal = true;
|
|
1956
|
+
push("final", response);
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
if (typed.type === "error") {
|
|
1960
|
+
push("error", {
|
|
1961
|
+
code: "CHAT_TURN_FAILED",
|
|
1962
|
+
message: formatUserFacingError(typed.error)
|
|
1963
|
+
});
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
if (!hasFinal) {
|
|
1968
|
+
const latestRun = await getRun({ runId: run.runId });
|
|
1969
|
+
if (latestRun?.state === "failed") {
|
|
1970
|
+
push("error", {
|
|
1971
|
+
code: "CHAT_TURN_FAILED",
|
|
1972
|
+
message: formatUserFacingError(latestRun.error ?? "chat run failed")
|
|
1973
|
+
});
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
push("done", { ok: true });
|
|
1978
|
+
} catch (error) {
|
|
1979
|
+
push("error", {
|
|
1980
|
+
code: "CHAT_TURN_FAILED",
|
|
1981
|
+
message: formatUserFacingError(error)
|
|
1982
|
+
});
|
|
1983
|
+
} finally {
|
|
1984
|
+
controller.close();
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
return new Response(stream, {
|
|
1989
|
+
status: 200,
|
|
1990
|
+
headers: {
|
|
1991
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
1992
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1993
|
+
"Connection": "keep-alive",
|
|
1994
|
+
"X-Accel-Buffering": "no"
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
};
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
// src/ui/provider-auth.ts
|
|
2001
|
+
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
2002
|
+
import { readFile } from "fs/promises";
|
|
2003
|
+
import { homedir } from "os";
|
|
2004
|
+
import { isAbsolute, resolve } from "path";
|
|
2005
|
+
import {
|
|
2006
|
+
ConfigSchema as ConfigSchema2,
|
|
2007
|
+
loadConfig as loadConfig2,
|
|
2008
|
+
saveConfig as saveConfig2
|
|
2009
|
+
} from "@nextclaw/core";
|
|
2010
|
+
var authSessions = /* @__PURE__ */ new Map();
|
|
2011
|
+
var DEFAULT_AUTH_INTERVAL_MS = 2e3;
|
|
2012
|
+
var MAX_AUTH_INTERVAL_MS = 1e4;
|
|
2013
|
+
function normalizePositiveInt(value, fallback) {
|
|
2014
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
2015
|
+
return fallback;
|
|
2016
|
+
}
|
|
2017
|
+
return Math.floor(value);
|
|
2018
|
+
}
|
|
2019
|
+
function normalizePositiveFloat(value) {
|
|
2020
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
return value;
|
|
2024
|
+
}
|
|
2025
|
+
function toBase64Url(buffer) {
|
|
2026
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
2027
|
+
}
|
|
2028
|
+
function buildPkce() {
|
|
2029
|
+
const verifier = toBase64Url(randomBytes(48));
|
|
2030
|
+
const challenge = toBase64Url(createHash("sha256").update(verifier).digest());
|
|
2031
|
+
return { verifier, challenge };
|
|
2032
|
+
}
|
|
2033
|
+
function withTrailingSlash(value) {
|
|
2034
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
2035
|
+
}
|
|
2036
|
+
function cleanupExpiredAuthSessions(now = Date.now()) {
|
|
2037
|
+
for (const [sessionId, session] of authSessions.entries()) {
|
|
2038
|
+
if (session.expiresAtMs <= now) {
|
|
2039
|
+
authSessions.delete(sessionId);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
function resolveDeviceCodeEndpoints(baseUrl, deviceCodePath, tokenPath) {
|
|
2044
|
+
const deviceCodeEndpoint = new URL(deviceCodePath, withTrailingSlash(baseUrl)).toString();
|
|
2045
|
+
const tokenEndpoint = new URL(tokenPath, withTrailingSlash(baseUrl)).toString();
|
|
2046
|
+
return { deviceCodeEndpoint, tokenEndpoint };
|
|
2047
|
+
}
|
|
2048
|
+
function resolveAuthNote(params) {
|
|
2049
|
+
return params.zh ?? params.en;
|
|
2050
|
+
}
|
|
2051
|
+
function resolveLocalizedMethodLabel(method, fallbackId) {
|
|
2052
|
+
return method.label?.zh ?? method.label?.en ?? fallbackId;
|
|
2053
|
+
}
|
|
2054
|
+
function resolveLocalizedMethodHint(method) {
|
|
2055
|
+
return method.hint?.zh ?? method.hint?.en;
|
|
2056
|
+
}
|
|
2057
|
+
function normalizeMethodId(value) {
|
|
2058
|
+
if (typeof value !== "string") {
|
|
2059
|
+
return void 0;
|
|
2060
|
+
}
|
|
2061
|
+
const trimmed = value.trim();
|
|
2062
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
2063
|
+
}
|
|
2064
|
+
function resolveAuthMethod(auth, requestedMethodId) {
|
|
2065
|
+
const protocol = auth.protocol ?? "rfc8628";
|
|
2066
|
+
const methods = (auth.methods ?? []).filter((entry) => normalizeMethodId(entry.id));
|
|
2067
|
+
const cleanRequestedMethodId = normalizeMethodId(requestedMethodId);
|
|
2068
|
+
if (methods.length === 0) {
|
|
2069
|
+
if (cleanRequestedMethodId) {
|
|
2070
|
+
throw new Error(`provider auth method is not supported: ${cleanRequestedMethodId}`);
|
|
2071
|
+
}
|
|
2072
|
+
return {
|
|
2073
|
+
protocol,
|
|
2074
|
+
baseUrl: auth.baseUrl,
|
|
2075
|
+
deviceCodePath: auth.deviceCodePath,
|
|
2076
|
+
tokenPath: auth.tokenPath,
|
|
2077
|
+
clientId: auth.clientId,
|
|
2078
|
+
scope: auth.scope,
|
|
2079
|
+
grantType: auth.grantType,
|
|
2080
|
+
usePkce: Boolean(auth.usePkce)
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
let selectedMethod = methods.find((entry) => normalizeMethodId(entry.id) === cleanRequestedMethodId);
|
|
2084
|
+
if (!selectedMethod) {
|
|
2085
|
+
const fallbackMethodId = normalizeMethodId(auth.defaultMethodId) ?? normalizeMethodId(methods[0]?.id);
|
|
2086
|
+
selectedMethod = methods.find((entry) => normalizeMethodId(entry.id) === fallbackMethodId) ?? methods[0];
|
|
2087
|
+
}
|
|
2088
|
+
const methodId = normalizeMethodId(selectedMethod?.id);
|
|
2089
|
+
if (!selectedMethod || !methodId) {
|
|
2090
|
+
throw new Error("provider auth method is not configured");
|
|
2091
|
+
}
|
|
2092
|
+
if (cleanRequestedMethodId && methodId !== cleanRequestedMethodId) {
|
|
2093
|
+
throw new Error(`provider auth method is not supported: ${cleanRequestedMethodId}`);
|
|
2094
|
+
}
|
|
2095
|
+
return {
|
|
2096
|
+
id: methodId,
|
|
2097
|
+
protocol,
|
|
2098
|
+
baseUrl: selectedMethod.baseUrl ?? auth.baseUrl,
|
|
2099
|
+
deviceCodePath: selectedMethod.deviceCodePath ?? auth.deviceCodePath,
|
|
2100
|
+
tokenPath: selectedMethod.tokenPath ?? auth.tokenPath,
|
|
2101
|
+
clientId: selectedMethod.clientId ?? auth.clientId,
|
|
2102
|
+
scope: selectedMethod.scope ?? auth.scope,
|
|
2103
|
+
grantType: selectedMethod.grantType ?? auth.grantType,
|
|
2104
|
+
usePkce: selectedMethod.usePkce ?? Boolean(auth.usePkce),
|
|
2105
|
+
defaultApiBase: selectedMethod.defaultApiBase
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
function parseExpiresAtMs(value, fallbackFromNowMs) {
|
|
2109
|
+
const normalized = normalizePositiveFloat(value);
|
|
2110
|
+
if (normalized === null) {
|
|
2111
|
+
return Date.now() + fallbackFromNowMs;
|
|
2112
|
+
}
|
|
2113
|
+
if (normalized >= 1e12) {
|
|
2114
|
+
return Math.floor(normalized);
|
|
2115
|
+
}
|
|
2116
|
+
if (normalized >= 1e9) {
|
|
2117
|
+
return Math.floor(normalized * 1e3);
|
|
2118
|
+
}
|
|
2119
|
+
return Date.now() + Math.floor(normalized * 1e3);
|
|
2120
|
+
}
|
|
2121
|
+
function parsePollIntervalMs(value, fallbackMs) {
|
|
2122
|
+
const normalized = normalizePositiveFloat(value);
|
|
2123
|
+
if (normalized === null) {
|
|
2124
|
+
return fallbackMs;
|
|
2125
|
+
}
|
|
2126
|
+
if (normalized <= 30) {
|
|
2127
|
+
return Math.floor(normalized * 1e3);
|
|
2128
|
+
}
|
|
2129
|
+
return Math.floor(normalized);
|
|
2130
|
+
}
|
|
2131
|
+
function buildMinimaxErrorMessage(payload, fallback) {
|
|
2132
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
2133
|
+
return fallback;
|
|
2134
|
+
}
|
|
2135
|
+
const record = payload;
|
|
2136
|
+
if (typeof record.error_description === "string" && record.error_description.trim()) {
|
|
2137
|
+
return record.error_description.trim();
|
|
2138
|
+
}
|
|
2139
|
+
if (typeof record.error === "string" && record.error.trim()) {
|
|
2140
|
+
return record.error.trim();
|
|
2141
|
+
}
|
|
2142
|
+
const baseMessage = record.base_resp?.status_msg;
|
|
2143
|
+
if (typeof baseMessage === "string" && baseMessage.trim()) {
|
|
2144
|
+
return baseMessage.trim();
|
|
2145
|
+
}
|
|
2146
|
+
return fallback;
|
|
2147
|
+
}
|
|
2148
|
+
function classifyMiniMaxErrorStatus(message) {
|
|
2149
|
+
const normalized = message.toLowerCase();
|
|
2150
|
+
if (normalized.includes("deny") || normalized.includes("rejected")) {
|
|
2151
|
+
return "denied";
|
|
2152
|
+
}
|
|
2153
|
+
if (normalized.includes("expired") || normalized.includes("timeout") || normalized.includes("timed out")) {
|
|
2154
|
+
return "expired";
|
|
2155
|
+
}
|
|
2156
|
+
return "error";
|
|
2157
|
+
}
|
|
2158
|
+
function resolveHomePath(inputPath) {
|
|
2159
|
+
const trimmed = inputPath.trim();
|
|
2160
|
+
if (!trimmed) {
|
|
2161
|
+
return trimmed;
|
|
2162
|
+
}
|
|
2163
|
+
if (trimmed === "~") {
|
|
2164
|
+
return homedir();
|
|
2165
|
+
}
|
|
2166
|
+
if (trimmed.startsWith("~/")) {
|
|
2167
|
+
return resolve(homedir(), trimmed.slice(2));
|
|
2168
|
+
}
|
|
2169
|
+
if (isAbsolute(trimmed)) {
|
|
2170
|
+
return trimmed;
|
|
2171
|
+
}
|
|
2172
|
+
return resolve(trimmed);
|
|
2173
|
+
}
|
|
2174
|
+
function normalizeExpiresAt(value) {
|
|
2175
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
2176
|
+
return Math.floor(value);
|
|
2177
|
+
}
|
|
2178
|
+
if (typeof value === "string" && value.trim()) {
|
|
2179
|
+
const asNumber = Number(value);
|
|
2180
|
+
if (Number.isFinite(asNumber) && asNumber > 0) {
|
|
2181
|
+
return Math.floor(asNumber);
|
|
2182
|
+
}
|
|
2183
|
+
const parsedTime = Date.parse(value);
|
|
2184
|
+
if (Number.isFinite(parsedTime) && parsedTime > 0) {
|
|
2185
|
+
return parsedTime;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return null;
|
|
2189
|
+
}
|
|
2190
|
+
function readFieldAsString(source, fieldName) {
|
|
2191
|
+
if (!fieldName) {
|
|
2192
|
+
return null;
|
|
2193
|
+
}
|
|
2194
|
+
const rawValue = source[fieldName];
|
|
2195
|
+
if (typeof rawValue !== "string") {
|
|
2196
|
+
return null;
|
|
2197
|
+
}
|
|
2198
|
+
const trimmed = rawValue.trim();
|
|
2199
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
2200
|
+
}
|
|
2201
|
+
function setProviderApiKey(params) {
|
|
2202
|
+
const config = loadConfig2(params.configPath);
|
|
2203
|
+
const providers = config.providers;
|
|
2204
|
+
if (!providers[params.provider]) {
|
|
2205
|
+
providers[params.provider] = {
|
|
2206
|
+
displayName: "",
|
|
2207
|
+
apiKey: "",
|
|
2208
|
+
apiBase: null,
|
|
2209
|
+
extraHeaders: null,
|
|
2210
|
+
wireApi: "auto",
|
|
2211
|
+
models: [],
|
|
2212
|
+
modelThinking: {}
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
const target = providers[params.provider];
|
|
2216
|
+
target.apiKey = params.accessToken;
|
|
2217
|
+
if (!target.apiBase && params.defaultApiBase) {
|
|
2218
|
+
target.apiBase = params.defaultApiBase;
|
|
2219
|
+
}
|
|
2220
|
+
const next = ConfigSchema2.parse(config);
|
|
2221
|
+
saveConfig2(next, params.configPath);
|
|
2222
|
+
}
|
|
2223
|
+
async function startProviderAuth(configPath, providerName, options) {
|
|
2224
|
+
cleanupExpiredAuthSessions();
|
|
2225
|
+
const spec = findServerBuiltinProviderByName(providerName);
|
|
2226
|
+
if (!spec?.auth || spec.auth.kind !== "device_code") {
|
|
2227
|
+
return null;
|
|
2228
|
+
}
|
|
2229
|
+
const resolvedMethod = resolveAuthMethod(spec.auth, options?.methodId);
|
|
2230
|
+
const { deviceCodeEndpoint, tokenEndpoint } = resolveDeviceCodeEndpoints(
|
|
2231
|
+
resolvedMethod.baseUrl,
|
|
2232
|
+
resolvedMethod.deviceCodePath,
|
|
2233
|
+
resolvedMethod.tokenPath
|
|
2234
|
+
);
|
|
2235
|
+
const pkce = resolvedMethod.usePkce ? buildPkce() : null;
|
|
2236
|
+
let authorizationCode = "";
|
|
2237
|
+
let tokenCodeField = "device_code";
|
|
2238
|
+
let userCode = "";
|
|
2239
|
+
let verificationUri = "";
|
|
2240
|
+
let intervalMs = DEFAULT_AUTH_INTERVAL_MS;
|
|
2241
|
+
let expiresAtMs = Date.now() + 6e5;
|
|
2242
|
+
if (resolvedMethod.protocol === "minimax_user_code") {
|
|
2243
|
+
if (!pkce) {
|
|
2244
|
+
throw new Error("MiniMax OAuth requires PKCE");
|
|
2245
|
+
}
|
|
2246
|
+
const state = toBase64Url(randomBytes(16));
|
|
2247
|
+
const body = new URLSearchParams({
|
|
2248
|
+
response_type: "code",
|
|
2249
|
+
client_id: resolvedMethod.clientId,
|
|
2250
|
+
scope: resolvedMethod.scope,
|
|
2251
|
+
code_challenge: pkce.challenge,
|
|
2252
|
+
code_challenge_method: "S256",
|
|
2253
|
+
state
|
|
2254
|
+
});
|
|
2255
|
+
const response = await fetch(deviceCodeEndpoint, {
|
|
2256
|
+
method: "POST",
|
|
2257
|
+
headers: {
|
|
2258
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2259
|
+
Accept: "application/json",
|
|
2260
|
+
"x-request-id": randomUUID()
|
|
2261
|
+
},
|
|
2262
|
+
body
|
|
2263
|
+
});
|
|
2264
|
+
const payload = await response.json().catch(() => ({}));
|
|
2265
|
+
if (!response.ok) {
|
|
2266
|
+
throw new Error(buildMinimaxErrorMessage(payload, response.statusText || "MiniMax OAuth start failed"));
|
|
2267
|
+
}
|
|
2268
|
+
if (payload.state && payload.state !== state) {
|
|
2269
|
+
throw new Error("MiniMax OAuth state mismatch");
|
|
2270
|
+
}
|
|
2271
|
+
authorizationCode = payload.user_code?.trim() ?? "";
|
|
2272
|
+
userCode = authorizationCode;
|
|
2273
|
+
verificationUri = payload.verification_uri?.trim() ?? "";
|
|
2274
|
+
if (!authorizationCode || !verificationUri) {
|
|
2275
|
+
throw new Error("provider auth payload is incomplete");
|
|
2276
|
+
}
|
|
2277
|
+
tokenCodeField = "user_code";
|
|
2278
|
+
intervalMs = Math.min(parsePollIntervalMs(payload.interval, DEFAULT_AUTH_INTERVAL_MS), MAX_AUTH_INTERVAL_MS);
|
|
2279
|
+
expiresAtMs = parseExpiresAtMs(payload.expired_in, 6e5);
|
|
2280
|
+
} else {
|
|
2281
|
+
const body = new URLSearchParams({
|
|
2282
|
+
client_id: resolvedMethod.clientId,
|
|
2283
|
+
scope: resolvedMethod.scope
|
|
2284
|
+
});
|
|
2285
|
+
if (pkce) {
|
|
2286
|
+
body.set("code_challenge", pkce.challenge);
|
|
2287
|
+
body.set("code_challenge_method", "S256");
|
|
2288
|
+
}
|
|
2289
|
+
const response = await fetch(deviceCodeEndpoint, {
|
|
2290
|
+
method: "POST",
|
|
2291
|
+
headers: {
|
|
2292
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2293
|
+
Accept: "application/json"
|
|
2294
|
+
},
|
|
2295
|
+
body
|
|
2296
|
+
});
|
|
2297
|
+
const payload = await response.json().catch(() => ({}));
|
|
2298
|
+
if (!response.ok) {
|
|
2299
|
+
const message = payload.error_description || payload.error || response.statusText || "device code auth failed";
|
|
2300
|
+
throw new Error(message);
|
|
2301
|
+
}
|
|
2302
|
+
authorizationCode = payload.device_code?.trim() ?? "";
|
|
2303
|
+
userCode = payload.user_code?.trim() ?? "";
|
|
2304
|
+
verificationUri = payload.verification_uri_complete?.trim() || payload.verification_uri?.trim() || "";
|
|
2305
|
+
if (!authorizationCode || !userCode || !verificationUri) {
|
|
2306
|
+
throw new Error("provider auth payload is incomplete");
|
|
2307
|
+
}
|
|
2308
|
+
intervalMs = normalizePositiveInt(payload.interval, DEFAULT_AUTH_INTERVAL_MS / 1e3) * 1e3;
|
|
2309
|
+
const expiresInSec = normalizePositiveInt(payload.expires_in, 600);
|
|
2310
|
+
expiresAtMs = Date.now() + expiresInSec * 1e3;
|
|
2311
|
+
}
|
|
2312
|
+
const sessionId = randomUUID();
|
|
2313
|
+
authSessions.set(sessionId, {
|
|
2314
|
+
sessionId,
|
|
2315
|
+
provider: providerName,
|
|
2316
|
+
configPath,
|
|
2317
|
+
authorizationCode,
|
|
2318
|
+
tokenCodeField,
|
|
2319
|
+
protocol: resolvedMethod.protocol,
|
|
2320
|
+
methodId: resolvedMethod.id,
|
|
2321
|
+
codeVerifier: pkce?.verifier,
|
|
2322
|
+
tokenEndpoint,
|
|
2323
|
+
clientId: resolvedMethod.clientId,
|
|
2324
|
+
grantType: resolvedMethod.grantType,
|
|
2325
|
+
defaultApiBase: resolvedMethod.defaultApiBase ?? spec.defaultApiBase,
|
|
2326
|
+
expiresAtMs,
|
|
2327
|
+
intervalMs
|
|
2328
|
+
});
|
|
2329
|
+
const methodConfig = (spec.auth.methods ?? []).find((entry) => normalizeMethodId(entry.id) === resolvedMethod.id);
|
|
2330
|
+
const methodLabel = methodConfig ? resolveLocalizedMethodLabel(methodConfig, resolvedMethod.id ?? "") : void 0;
|
|
2331
|
+
const methodHint = methodConfig ? resolveLocalizedMethodHint(methodConfig) : void 0;
|
|
2332
|
+
return {
|
|
2333
|
+
provider: providerName,
|
|
2334
|
+
kind: "device_code",
|
|
2335
|
+
methodId: resolvedMethod.id,
|
|
2336
|
+
sessionId,
|
|
2337
|
+
verificationUri,
|
|
2338
|
+
userCode,
|
|
2339
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
2340
|
+
intervalMs,
|
|
2341
|
+
note: methodHint ?? methodLabel ?? resolveAuthNote(spec.auth.note ?? {})
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
async function pollProviderAuth(params) {
|
|
2345
|
+
cleanupExpiredAuthSessions();
|
|
2346
|
+
const session = authSessions.get(params.sessionId);
|
|
2347
|
+
if (!session || session.provider !== params.providerName || session.configPath !== params.configPath) {
|
|
2348
|
+
return null;
|
|
2349
|
+
}
|
|
2350
|
+
if (Date.now() >= session.expiresAtMs) {
|
|
2351
|
+
authSessions.delete(params.sessionId);
|
|
2352
|
+
return {
|
|
2353
|
+
provider: params.providerName,
|
|
2354
|
+
status: "expired",
|
|
2355
|
+
message: "authorization session expired"
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
const body = new URLSearchParams({
|
|
2359
|
+
grant_type: session.grantType,
|
|
2360
|
+
client_id: session.clientId
|
|
2361
|
+
});
|
|
2362
|
+
body.set(session.tokenCodeField, session.authorizationCode);
|
|
2363
|
+
if (session.codeVerifier) {
|
|
2364
|
+
body.set("code_verifier", session.codeVerifier);
|
|
2365
|
+
}
|
|
2366
|
+
const response = await fetch(session.tokenEndpoint, {
|
|
2367
|
+
method: "POST",
|
|
2368
|
+
headers: {
|
|
2369
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2370
|
+
Accept: "application/json"
|
|
2371
|
+
},
|
|
2372
|
+
body
|
|
2373
|
+
});
|
|
2374
|
+
let accessToken = "";
|
|
2375
|
+
if (session.protocol === "minimax_user_code") {
|
|
2376
|
+
const raw = await response.text();
|
|
2377
|
+
let payload = {};
|
|
2378
|
+
if (raw) {
|
|
2379
|
+
try {
|
|
2380
|
+
payload = JSON.parse(raw);
|
|
2381
|
+
} catch {
|
|
2382
|
+
payload = {};
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (!response.ok) {
|
|
2386
|
+
const message = buildMinimaxErrorMessage(payload, raw || response.statusText || "authorization failed");
|
|
2387
|
+
return {
|
|
2388
|
+
provider: params.providerName,
|
|
2389
|
+
status: "error",
|
|
2390
|
+
message
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
const status = payload.status?.trim().toLowerCase();
|
|
2394
|
+
if (status === "success") {
|
|
2395
|
+
accessToken = payload.access_token?.trim() ?? "";
|
|
2396
|
+
if (!accessToken) {
|
|
2397
|
+
return {
|
|
2398
|
+
provider: params.providerName,
|
|
2399
|
+
status: "error",
|
|
2400
|
+
message: "provider token response missing access token"
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
} else if (status === "error") {
|
|
2404
|
+
const message = buildMinimaxErrorMessage(payload, "authorization failed");
|
|
2405
|
+
const classified = classifyMiniMaxErrorStatus(message);
|
|
2406
|
+
if (classified === "denied" || classified === "expired") {
|
|
2407
|
+
authSessions.delete(params.sessionId);
|
|
2408
|
+
}
|
|
2409
|
+
return {
|
|
2410
|
+
provider: params.providerName,
|
|
2411
|
+
status: classified,
|
|
2412
|
+
message
|
|
2413
|
+
};
|
|
2414
|
+
} else {
|
|
2415
|
+
const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
|
|
2416
|
+
session.intervalMs = nextPollMs;
|
|
2417
|
+
authSessions.set(params.sessionId, session);
|
|
2418
|
+
return {
|
|
2419
|
+
provider: params.providerName,
|
|
2420
|
+
status: "pending",
|
|
2421
|
+
nextPollMs
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
} else {
|
|
2425
|
+
const payload = await response.json().catch(() => ({}));
|
|
2426
|
+
if (!response.ok) {
|
|
2427
|
+
const errorCode = payload.error?.trim().toLowerCase();
|
|
2428
|
+
if (errorCode === "authorization_pending") {
|
|
2429
|
+
return {
|
|
2430
|
+
provider: params.providerName,
|
|
2431
|
+
status: "pending",
|
|
2432
|
+
nextPollMs: session.intervalMs
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
if (errorCode === "slow_down") {
|
|
2436
|
+
const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
|
|
2437
|
+
session.intervalMs = nextPollMs;
|
|
2438
|
+
authSessions.set(params.sessionId, session);
|
|
2439
|
+
return {
|
|
2440
|
+
provider: params.providerName,
|
|
2441
|
+
status: "pending",
|
|
2442
|
+
nextPollMs
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
if (errorCode === "access_denied") {
|
|
2446
|
+
authSessions.delete(params.sessionId);
|
|
2447
|
+
return {
|
|
2448
|
+
provider: params.providerName,
|
|
2449
|
+
status: "denied",
|
|
2450
|
+
message: payload.error_description || "authorization denied"
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
if (errorCode === "expired_token") {
|
|
2454
|
+
authSessions.delete(params.sessionId);
|
|
2455
|
+
return {
|
|
2456
|
+
provider: params.providerName,
|
|
2457
|
+
status: "expired",
|
|
2458
|
+
message: payload.error_description || "authorization session expired"
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
return {
|
|
2462
|
+
provider: params.providerName,
|
|
2463
|
+
status: "error",
|
|
2464
|
+
message: payload.error_description || payload.error || response.statusText || "authorization failed"
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
accessToken = payload.access_token?.trim() ?? "";
|
|
2468
|
+
if (!accessToken) {
|
|
2469
|
+
return {
|
|
2470
|
+
provider: params.providerName,
|
|
2471
|
+
status: "error",
|
|
2472
|
+
message: "provider token response missing access token"
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
setProviderApiKey({
|
|
2477
|
+
configPath: params.configPath,
|
|
2478
|
+
provider: params.providerName,
|
|
2479
|
+
accessToken,
|
|
2480
|
+
defaultApiBase: session.defaultApiBase
|
|
2481
|
+
});
|
|
2482
|
+
authSessions.delete(params.sessionId);
|
|
2483
|
+
return {
|
|
2484
|
+
provider: params.providerName,
|
|
2485
|
+
status: "authorized"
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
async function importProviderAuthFromCli(configPath, providerName) {
|
|
2489
|
+
const spec = findServerBuiltinProviderByName(providerName);
|
|
2490
|
+
if (!spec?.auth || spec.auth.kind !== "device_code" || !spec.auth.cliCredential) {
|
|
2491
|
+
return null;
|
|
2492
|
+
}
|
|
2493
|
+
const credentialPath = resolveHomePath(spec.auth.cliCredential.path);
|
|
2494
|
+
if (!credentialPath) {
|
|
2495
|
+
throw new Error("provider cli credential path is empty");
|
|
2496
|
+
}
|
|
2497
|
+
let rawContent = "";
|
|
2498
|
+
try {
|
|
2499
|
+
rawContent = await readFile(credentialPath, "utf8");
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2502
|
+
throw new Error(`failed to read CLI credential: ${message}`);
|
|
2503
|
+
}
|
|
2504
|
+
let payload;
|
|
2505
|
+
try {
|
|
2506
|
+
const parsed = JSON.parse(rawContent);
|
|
1839
2507
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1840
2508
|
throw new Error("credential payload is not an object");
|
|
1841
2509
|
}
|
|
1842
|
-
payload = parsed;
|
|
2510
|
+
payload = parsed;
|
|
2511
|
+
} catch (error) {
|
|
2512
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2513
|
+
throw new Error(`invalid CLI credential JSON: ${message}`);
|
|
2514
|
+
}
|
|
2515
|
+
const accessToken = readFieldAsString(payload, spec.auth.cliCredential.accessTokenField);
|
|
2516
|
+
if (!accessToken) {
|
|
2517
|
+
throw new Error(
|
|
2518
|
+
`CLI credential missing access token field: ${spec.auth.cliCredential.accessTokenField}`
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
const expiresAtMs = normalizeExpiresAt(
|
|
2522
|
+
spec.auth.cliCredential.expiresAtField ? payload[spec.auth.cliCredential.expiresAtField] : void 0
|
|
2523
|
+
);
|
|
2524
|
+
if (typeof expiresAtMs === "number" && expiresAtMs <= Date.now()) {
|
|
2525
|
+
throw new Error("CLI credential has expired, please login again");
|
|
2526
|
+
}
|
|
2527
|
+
setProviderApiKey({
|
|
2528
|
+
configPath,
|
|
2529
|
+
provider: providerName,
|
|
2530
|
+
accessToken,
|
|
2531
|
+
defaultApiBase: spec.defaultApiBase
|
|
2532
|
+
});
|
|
2533
|
+
return {
|
|
2534
|
+
provider: providerName,
|
|
2535
|
+
status: "imported",
|
|
2536
|
+
source: "cli",
|
|
2537
|
+
expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : void 0
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
// src/ui/router/config.controller.ts
|
|
2542
|
+
var ConfigRoutesController = class {
|
|
2543
|
+
constructor(options) {
|
|
2544
|
+
this.options = options;
|
|
2545
|
+
}
|
|
2546
|
+
getConfig = (c) => {
|
|
2547
|
+
const config = loadConfigOrDefault(this.options.configPath);
|
|
2548
|
+
return c.json(ok(buildConfigView(config)));
|
|
2549
|
+
};
|
|
2550
|
+
getConfigMeta = (c) => {
|
|
2551
|
+
const config = loadConfigOrDefault(this.options.configPath);
|
|
2552
|
+
return c.json(ok(buildConfigMeta(config)));
|
|
2553
|
+
};
|
|
2554
|
+
getConfigSchema = (c) => {
|
|
2555
|
+
const config = loadConfigOrDefault(this.options.configPath);
|
|
2556
|
+
return c.json(ok(buildConfigSchemaView(config)));
|
|
2557
|
+
};
|
|
2558
|
+
updateConfigModel = async (c) => {
|
|
2559
|
+
const body = await readJson(c.req.raw);
|
|
2560
|
+
if (!body.ok) {
|
|
2561
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2562
|
+
}
|
|
2563
|
+
const hasModel = typeof body.data.model === "string";
|
|
2564
|
+
if (!hasModel) {
|
|
2565
|
+
return c.json(err("INVALID_BODY", "model is required"), 400);
|
|
2566
|
+
}
|
|
2567
|
+
const view = updateModel(this.options.configPath, {
|
|
2568
|
+
model: body.data.model
|
|
2569
|
+
});
|
|
2570
|
+
if (hasModel) {
|
|
2571
|
+
this.options.publish({ type: "config.updated", payload: { path: "agents.defaults.model" } });
|
|
2572
|
+
}
|
|
2573
|
+
return c.json(ok({
|
|
2574
|
+
model: view.agents.defaults.model
|
|
2575
|
+
}));
|
|
2576
|
+
};
|
|
2577
|
+
updateConfigSearch = async (c) => {
|
|
2578
|
+
const body = await readJson(c.req.raw);
|
|
2579
|
+
if (!body.ok) {
|
|
2580
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2581
|
+
}
|
|
2582
|
+
const result = updateSearch(this.options.configPath, body.data);
|
|
2583
|
+
this.options.publish({ type: "config.updated", payload: { path: "search" } });
|
|
2584
|
+
return c.json(ok(result));
|
|
2585
|
+
};
|
|
2586
|
+
updateProvider = async (c) => {
|
|
2587
|
+
const provider = c.req.param("provider");
|
|
2588
|
+
const body = await readJson(c.req.raw);
|
|
2589
|
+
if (!body.ok) {
|
|
2590
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2591
|
+
}
|
|
2592
|
+
const result = updateProvider(this.options.configPath, provider, body.data);
|
|
2593
|
+
if (!result) {
|
|
2594
|
+
return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
|
|
2595
|
+
}
|
|
2596
|
+
this.options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
2597
|
+
return c.json(ok(result));
|
|
2598
|
+
};
|
|
2599
|
+
createProvider = async (c) => {
|
|
2600
|
+
const body = await readJson(c.req.raw);
|
|
2601
|
+
if (!body.ok) {
|
|
2602
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2603
|
+
}
|
|
2604
|
+
const result = createCustomProvider(
|
|
2605
|
+
this.options.configPath,
|
|
2606
|
+
body.data
|
|
2607
|
+
);
|
|
2608
|
+
this.options.publish({ type: "config.updated", payload: { path: `providers.${result.name}` } });
|
|
2609
|
+
return c.json(ok({
|
|
2610
|
+
name: result.name,
|
|
2611
|
+
provider: result.provider
|
|
2612
|
+
}));
|
|
2613
|
+
};
|
|
2614
|
+
deleteProvider = async (c) => {
|
|
2615
|
+
const provider = c.req.param("provider");
|
|
2616
|
+
const result = deleteCustomProvider(this.options.configPath, provider);
|
|
2617
|
+
if (result === null) {
|
|
2618
|
+
return c.json(err("NOT_FOUND", `custom provider not found: ${provider}`), 404);
|
|
2619
|
+
}
|
|
2620
|
+
this.options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
2621
|
+
return c.json(ok({
|
|
2622
|
+
deleted: true,
|
|
2623
|
+
provider
|
|
2624
|
+
}));
|
|
2625
|
+
};
|
|
2626
|
+
testProviderConnection = async (c) => {
|
|
2627
|
+
const provider = c.req.param("provider");
|
|
2628
|
+
const body = await readJson(c.req.raw);
|
|
2629
|
+
if (!body.ok) {
|
|
2630
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2631
|
+
}
|
|
2632
|
+
const result = await testProviderConnection(
|
|
2633
|
+
this.options.configPath,
|
|
2634
|
+
provider,
|
|
2635
|
+
body.data
|
|
2636
|
+
);
|
|
2637
|
+
if (!result) {
|
|
2638
|
+
return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
|
|
2639
|
+
}
|
|
2640
|
+
return c.json(ok(result));
|
|
2641
|
+
};
|
|
2642
|
+
startProviderAuth = async (c) => {
|
|
2643
|
+
const provider = c.req.param("provider");
|
|
2644
|
+
let payload = {};
|
|
2645
|
+
const rawBody = await c.req.raw.text();
|
|
2646
|
+
if (rawBody.trim().length > 0) {
|
|
2647
|
+
try {
|
|
2648
|
+
payload = JSON.parse(rawBody);
|
|
2649
|
+
} catch {
|
|
2650
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
const methodId = typeof payload.methodId === "string" ? payload.methodId.trim() : void 0;
|
|
2654
|
+
try {
|
|
2655
|
+
const result = await startProviderAuth(this.options.configPath, provider, {
|
|
2656
|
+
methodId
|
|
2657
|
+
});
|
|
2658
|
+
if (!result) {
|
|
2659
|
+
return c.json(err("NOT_SUPPORTED", `provider auth is not supported: ${provider}`), 404);
|
|
2660
|
+
}
|
|
2661
|
+
return c.json(ok(result));
|
|
2662
|
+
} catch (error) {
|
|
2663
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2664
|
+
return c.json(err("AUTH_START_FAILED", message), 400);
|
|
2665
|
+
}
|
|
2666
|
+
};
|
|
2667
|
+
pollProviderAuth = async (c) => {
|
|
2668
|
+
const provider = c.req.param("provider");
|
|
2669
|
+
const body = await readJson(c.req.raw);
|
|
2670
|
+
if (!body.ok) {
|
|
2671
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2672
|
+
}
|
|
2673
|
+
const sessionId = typeof body.data.sessionId === "string" ? body.data.sessionId.trim() : "";
|
|
2674
|
+
if (!sessionId) {
|
|
2675
|
+
return c.json(err("INVALID_BODY", "sessionId is required"), 400);
|
|
2676
|
+
}
|
|
2677
|
+
const result = await pollProviderAuth({
|
|
2678
|
+
configPath: this.options.configPath,
|
|
2679
|
+
providerName: provider,
|
|
2680
|
+
sessionId
|
|
2681
|
+
});
|
|
2682
|
+
if (!result) {
|
|
2683
|
+
return c.json(err("NOT_FOUND", "provider auth session not found"), 404);
|
|
2684
|
+
}
|
|
2685
|
+
if (result.status === "authorized") {
|
|
2686
|
+
this.options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
2687
|
+
}
|
|
2688
|
+
return c.json(ok(result));
|
|
2689
|
+
};
|
|
2690
|
+
importProviderAuthFromCli = async (c) => {
|
|
2691
|
+
const provider = c.req.param("provider");
|
|
2692
|
+
try {
|
|
2693
|
+
const result = await importProviderAuthFromCli(this.options.configPath, provider);
|
|
2694
|
+
if (!result) {
|
|
2695
|
+
return c.json(err("NOT_SUPPORTED", `provider cli auth import is not supported: ${provider}`), 404);
|
|
2696
|
+
}
|
|
2697
|
+
this.options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
2698
|
+
return c.json(ok(result));
|
|
2699
|
+
} catch (error) {
|
|
2700
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2701
|
+
return c.json(err("AUTH_IMPORT_FAILED", message), 400);
|
|
2702
|
+
}
|
|
2703
|
+
};
|
|
2704
|
+
updateChannel = async (c) => {
|
|
2705
|
+
const channel = c.req.param("channel");
|
|
2706
|
+
const body = await readJson(c.req.raw);
|
|
2707
|
+
if (!body.ok) {
|
|
2708
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2709
|
+
}
|
|
2710
|
+
const result = updateChannel(this.options.configPath, channel, body.data);
|
|
2711
|
+
if (!result) {
|
|
2712
|
+
return c.json(err("NOT_FOUND", `unknown channel: ${channel}`), 404);
|
|
2713
|
+
}
|
|
2714
|
+
this.options.publish({ type: "config.updated", payload: { path: `channels.${channel}` } });
|
|
2715
|
+
return c.json(ok(result));
|
|
2716
|
+
};
|
|
2717
|
+
updateSecrets = async (c) => {
|
|
2718
|
+
const body = await readJson(c.req.raw);
|
|
2719
|
+
if (!body.ok) {
|
|
2720
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2721
|
+
}
|
|
2722
|
+
const result = updateSecrets(this.options.configPath, body.data);
|
|
2723
|
+
this.options.publish({ type: "config.updated", payload: { path: "secrets" } });
|
|
2724
|
+
return c.json(ok(result));
|
|
2725
|
+
};
|
|
2726
|
+
updateRuntime = async (c) => {
|
|
2727
|
+
const body = await readJson(c.req.raw);
|
|
2728
|
+
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
2729
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2730
|
+
}
|
|
2731
|
+
const result = updateRuntime(this.options.configPath, body.data);
|
|
2732
|
+
if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "contextTokens")) {
|
|
2733
|
+
this.options.publish({ type: "config.updated", payload: { path: "agents.defaults.contextTokens" } });
|
|
2734
|
+
}
|
|
2735
|
+
if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engine")) {
|
|
2736
|
+
this.options.publish({ type: "config.updated", payload: { path: "agents.defaults.engine" } });
|
|
2737
|
+
}
|
|
2738
|
+
if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engineConfig")) {
|
|
2739
|
+
this.options.publish({ type: "config.updated", payload: { path: "agents.defaults.engineConfig" } });
|
|
2740
|
+
}
|
|
2741
|
+
this.options.publish({ type: "config.updated", payload: { path: "agents.list" } });
|
|
2742
|
+
this.options.publish({ type: "config.updated", payload: { path: "bindings" } });
|
|
2743
|
+
this.options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
2744
|
+
return c.json(ok(result));
|
|
2745
|
+
};
|
|
2746
|
+
executeAction = async (c) => {
|
|
2747
|
+
const actionId = c.req.param("actionId");
|
|
2748
|
+
const body = await readJson(c.req.raw);
|
|
2749
|
+
if (!body.ok) {
|
|
2750
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2751
|
+
}
|
|
2752
|
+
const result = await executeConfigAction(this.options.configPath, actionId, body.data ?? {});
|
|
2753
|
+
if (!result.ok) {
|
|
2754
|
+
return c.json(err(result.code, result.message, result.details), 400);
|
|
2755
|
+
}
|
|
2756
|
+
return c.json(ok(result.data));
|
|
2757
|
+
};
|
|
2758
|
+
};
|
|
2759
|
+
|
|
2760
|
+
// src/ui/router/cron.controller.ts
|
|
2761
|
+
function toIsoTime(value) {
|
|
2762
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2763
|
+
return null;
|
|
2764
|
+
}
|
|
2765
|
+
const date = new Date(value);
|
|
2766
|
+
if (Number.isNaN(date.getTime())) {
|
|
2767
|
+
return null;
|
|
2768
|
+
}
|
|
2769
|
+
return date.toISOString();
|
|
2770
|
+
}
|
|
2771
|
+
function buildCronJobView(job) {
|
|
2772
|
+
return {
|
|
2773
|
+
id: job.id,
|
|
2774
|
+
name: job.name,
|
|
2775
|
+
enabled: job.enabled,
|
|
2776
|
+
schedule: job.schedule,
|
|
2777
|
+
payload: job.payload,
|
|
2778
|
+
state: {
|
|
2779
|
+
nextRunAt: toIsoTime(job.state.nextRunAtMs),
|
|
2780
|
+
lastRunAt: toIsoTime(job.state.lastRunAtMs),
|
|
2781
|
+
lastStatus: job.state.lastStatus ?? null,
|
|
2782
|
+
lastError: job.state.lastError ?? null
|
|
2783
|
+
},
|
|
2784
|
+
createdAt: new Date(job.createdAtMs).toISOString(),
|
|
2785
|
+
updatedAt: new Date(job.updatedAtMs).toISOString(),
|
|
2786
|
+
deleteAfterRun: job.deleteAfterRun
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
function findCronJob(service, id) {
|
|
2790
|
+
const jobs = service.listJobs(true);
|
|
2791
|
+
return jobs.find((job) => job.id === id) ?? null;
|
|
2792
|
+
}
|
|
2793
|
+
var CronRoutesController = class {
|
|
2794
|
+
constructor(options) {
|
|
2795
|
+
this.options = options;
|
|
2796
|
+
}
|
|
2797
|
+
listJobs = (c) => {
|
|
2798
|
+
if (!this.options.cronService) {
|
|
2799
|
+
return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
|
|
2800
|
+
}
|
|
2801
|
+
const query = c.req.query();
|
|
2802
|
+
const includeDisabled = query.all === "1" || query.all === "true" || query.all === "yes";
|
|
2803
|
+
const jobs = this.options.cronService.listJobs(includeDisabled).map((job) => buildCronJobView(job));
|
|
2804
|
+
return c.json(ok({ jobs, total: jobs.length }));
|
|
2805
|
+
};
|
|
2806
|
+
deleteJob = (c) => {
|
|
2807
|
+
if (!this.options.cronService) {
|
|
2808
|
+
return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
|
|
2809
|
+
}
|
|
2810
|
+
const id = decodeURIComponent(c.req.param("id"));
|
|
2811
|
+
const deleted = this.options.cronService.removeJob(id);
|
|
2812
|
+
if (!deleted) {
|
|
2813
|
+
return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
|
|
2814
|
+
}
|
|
2815
|
+
return c.json(ok({ deleted: true }));
|
|
2816
|
+
};
|
|
2817
|
+
enableJob = async (c) => {
|
|
2818
|
+
if (!this.options.cronService) {
|
|
2819
|
+
return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
|
|
2820
|
+
}
|
|
2821
|
+
const id = decodeURIComponent(c.req.param("id"));
|
|
2822
|
+
const body = await readJson(c.req.raw);
|
|
2823
|
+
if (!body.ok) {
|
|
2824
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2825
|
+
}
|
|
2826
|
+
if (typeof body.data.enabled !== "boolean") {
|
|
2827
|
+
return c.json(err("INVALID_BODY", "enabled must be boolean"), 400);
|
|
2828
|
+
}
|
|
2829
|
+
const job = this.options.cronService.enableJob(id, body.data.enabled);
|
|
2830
|
+
if (!job) {
|
|
2831
|
+
return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
|
|
2832
|
+
}
|
|
2833
|
+
const data = { job: buildCronJobView(job) };
|
|
2834
|
+
return c.json(ok(data));
|
|
2835
|
+
};
|
|
2836
|
+
runJob = async (c) => {
|
|
2837
|
+
if (!this.options.cronService) {
|
|
2838
|
+
return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
|
|
2839
|
+
}
|
|
2840
|
+
const id = decodeURIComponent(c.req.param("id"));
|
|
2841
|
+
const body = await readJson(c.req.raw);
|
|
2842
|
+
if (!body.ok) {
|
|
2843
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2844
|
+
}
|
|
2845
|
+
const existing = findCronJob(this.options.cronService, id);
|
|
2846
|
+
if (!existing) {
|
|
2847
|
+
return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
|
|
2848
|
+
}
|
|
2849
|
+
const executed = await this.options.cronService.runJob(id, Boolean(body.data.force));
|
|
2850
|
+
const after = findCronJob(this.options.cronService, id);
|
|
2851
|
+
const data = {
|
|
2852
|
+
job: after ? buildCronJobView(after) : null,
|
|
2853
|
+
executed
|
|
2854
|
+
};
|
|
2855
|
+
return c.json(ok(data));
|
|
2856
|
+
};
|
|
2857
|
+
};
|
|
2858
|
+
|
|
2859
|
+
// src/ui/router/marketplace/constants.ts
|
|
2860
|
+
var DEFAULT_MARKETPLACE_API_BASE = "https://marketplace-api.nextclaw.io";
|
|
2861
|
+
var NEXTCLAW_PLUGIN_NPM_PREFIX = "@nextclaw/channel-plugin-";
|
|
2862
|
+
var CLAWBAY_CHANNEL_PLUGIN_NPM_SPEC = "@clawbay/clawbay-channel";
|
|
2863
|
+
var BUILTIN_CHANNEL_PLUGIN_ID_PREFIX = "builtin-channel-";
|
|
2864
|
+
var MARKETPLACE_REMOTE_PAGE_SIZE = 100;
|
|
2865
|
+
var MARKETPLACE_REMOTE_MAX_PAGES = 20;
|
|
2866
|
+
var MARKETPLACE_ZH_COPY_BY_SLUG = {
|
|
2867
|
+
weather: {
|
|
2868
|
+
summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u5929\u6C14\u67E5\u8BE2\u5DE5\u4F5C\u6D41\u3002",
|
|
2869
|
+
description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u5FEB\u901F\u5929\u6C14\u67E5\u8BE2\u5DE5\u4F5C\u6D41\u3002"
|
|
2870
|
+
},
|
|
2871
|
+
summarize: {
|
|
2872
|
+
summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u7ED3\u6784\u5316\u6458\u8981\u3002",
|
|
2873
|
+
description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u6587\u4EF6\u4E0E\u957F\u6587\u672C\u7684\u6458\u8981\u5DE5\u4F5C\u6D41\u3002"
|
|
2874
|
+
},
|
|
2875
|
+
github: {
|
|
2876
|
+
summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E GitHub \u5DE5\u4F5C\u6D41\u3002",
|
|
2877
|
+
description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B Issue\u3001PR \u4E0E\u4ED3\u5E93\u76F8\u5173\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
|
|
2878
|
+
},
|
|
2879
|
+
tmux: {
|
|
2880
|
+
summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u7EC8\u7AEF/Tmux \u534F\u4F5C\u5DE5\u4F5C\u6D41\u3002",
|
|
2881
|
+
description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u57FA\u4E8E Tmux \u7684\u4EFB\u52A1\u6267\u884C\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
|
|
2882
|
+
},
|
|
2883
|
+
gog: {
|
|
2884
|
+
summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u56FE\u8C31\u5BFC\u5411\u751F\u6210\u5DE5\u4F5C\u6D41\u3002",
|
|
2885
|
+
description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u56FE\u8C31\u4E0E\u89C4\u5212\u5BFC\u5411\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
|
|
2886
|
+
},
|
|
2887
|
+
pdf: {
|
|
2888
|
+
summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E PDF \u8BFB\u53D6/\u5408\u5E76/\u62C6\u5206/OCR \u5DE5\u4F5C\u6D41\u3002",
|
|
2889
|
+
description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u8BFB\u53D6\u3001\u63D0\u53D6\u3001\u5408\u5E76\u3001\u62C6\u5206\u3001\u65CB\u8F6C\u5E76\u5BF9 PDF \u6267\u884C OCR \u5904\u7406\u3002"
|
|
2890
|
+
},
|
|
2891
|
+
docx: {
|
|
2892
|
+
summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u521B\u5EFA\u548C\u7F16\u8F91 Word \u6587\u6863\u3002",
|
|
2893
|
+
description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u521B\u5EFA\u3001\u8BFB\u53D6\u3001\u7F16\u8F91\u5E76\u91CD\u6784 .docx \u6587\u6863\u3002"
|
|
2894
|
+
},
|
|
2895
|
+
pptx: {
|
|
2896
|
+
summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u6F14\u793A\u6587\u7A3F\u64CD\u4F5C\u3002",
|
|
2897
|
+
description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u521B\u5EFA\u3001\u89E3\u6790\u3001\u7F16\u8F91\u5E76\u91CD\u7EC4 .pptx \u6F14\u793A\u6587\u7A3F\u3002"
|
|
2898
|
+
},
|
|
2899
|
+
xlsx: {
|
|
2900
|
+
summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u8868\u683C\u6587\u6863\u5DE5\u4F5C\u6D41\u3002",
|
|
2901
|
+
description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u6253\u5F00\u3001\u7F16\u8F91\u3001\u6E05\u6D17\u5E76\u8F6C\u6362 .xlsx \u4E0E .csv \u7B49\u8868\u683C\u6587\u4EF6\u3002"
|
|
2902
|
+
},
|
|
2903
|
+
bird: {
|
|
2904
|
+
summary: "OpenClaw \u793E\u533A\u6280\u80FD\uFF0C\u7528\u4E8E X/Twitter \u8BFB\u53D6/\u641C\u7D22/\u53D1\u5E03\u5DE5\u4F5C\u6D41\u3002",
|
|
2905
|
+
description: "\u4F7F\u7528 bird CLI \u5728\u4EE3\u7406\u5DE5\u4F5C\u6D41\u4E2D\u8BFB\u53D6\u7EBF\u7A0B\u3001\u641C\u7D22\u5E16\u5B50\u5E76\u8D77\u8349\u63A8\u6587/\u56DE\u590D\u3002"
|
|
2906
|
+
},
|
|
2907
|
+
"cloudflare-deploy": {
|
|
2908
|
+
summary: "OpenAI \u7CBE\u9009\u6280\u80FD\uFF0C\u7528\u4E8E\u5728 Cloudflare \u4E0A\u90E8\u7F72\u5E94\u7528\u4E0E\u57FA\u7840\u8BBE\u65BD\u3002",
|
|
2909
|
+
description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u9009\u62E9 Cloudflare \u4EA7\u54C1\u5E76\u90E8\u7F72 Workers\u3001Pages \u53CA\u76F8\u5173\u670D\u52A1\u3002"
|
|
2910
|
+
},
|
|
2911
|
+
"channel-plugin-discord": {
|
|
2912
|
+
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Discord \u6E20\u9053\u96C6\u6210\u3002",
|
|
2913
|
+
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Discord \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2914
|
+
},
|
|
2915
|
+
"channel-plugin-telegram": {
|
|
2916
|
+
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Telegram \u6E20\u9053\u96C6\u6210\u3002",
|
|
2917
|
+
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Telegram \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2918
|
+
},
|
|
2919
|
+
"channel-plugin-slack": {
|
|
2920
|
+
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Slack \u6E20\u9053\u96C6\u6210\u3002",
|
|
2921
|
+
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Slack \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2922
|
+
},
|
|
2923
|
+
"channel-plugin-wecom": {
|
|
2924
|
+
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E\u4F01\u4E1A\u5FAE\u4FE1\u6E20\u9053\u96C6\u6210\u3002",
|
|
2925
|
+
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B\u4F01\u4E1A\u5FAE\u4FE1\u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2926
|
+
},
|
|
2927
|
+
"channel-plugin-email": {
|
|
2928
|
+
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Email \u6E20\u9053\u96C6\u6210\u3002",
|
|
2929
|
+
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Email \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2930
|
+
},
|
|
2931
|
+
"channel-plugin-whatsapp": {
|
|
2932
|
+
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E WhatsApp \u6E20\u9053\u96C6\u6210\u3002",
|
|
2933
|
+
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B WhatsApp \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2934
|
+
},
|
|
2935
|
+
"channel-plugin-clawbay": {
|
|
2936
|
+
summary: "Clawbay \u5B98\u65B9\u6E20\u9053\u63D2\u4EF6\uFF0C\u7528\u4E8E NextClaw \u96C6\u6210\u3002",
|
|
2937
|
+
description: "\u901A\u8FC7\u63D2\u4EF6\u8FD0\u884C\u65F6\u4E3A NextClaw \u63D0\u4F9B Clawbay \u6E20\u9053\u80FD\u529B\u3002"
|
|
2938
|
+
}
|
|
2939
|
+
};
|
|
2940
|
+
|
|
2941
|
+
// src/ui/router/marketplace/catalog.ts
|
|
2942
|
+
function normalizeMarketplaceBaseUrl(options) {
|
|
2943
|
+
const configured = options.marketplace?.apiBaseUrl?.trim();
|
|
2944
|
+
if (!configured) {
|
|
2945
|
+
return DEFAULT_MARKETPLACE_API_BASE;
|
|
2946
|
+
}
|
|
2947
|
+
return configured.replace(/\/$/, "");
|
|
2948
|
+
}
|
|
2949
|
+
function toMarketplaceUrl(baseUrl, path, query = {}) {
|
|
2950
|
+
const url = new URL(path, `${baseUrl.replace(/\/$/, "")}/`);
|
|
2951
|
+
for (const [key, value] of Object.entries(query)) {
|
|
2952
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2953
|
+
url.searchParams.set(key, value);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
return url.toString();
|
|
2957
|
+
}
|
|
2958
|
+
async function fetchMarketplaceData(params) {
|
|
2959
|
+
const endpoint = toMarketplaceUrl(params.baseUrl, params.path, params.query);
|
|
2960
|
+
let response;
|
|
2961
|
+
try {
|
|
2962
|
+
response = await fetch(endpoint, {
|
|
2963
|
+
method: "GET",
|
|
2964
|
+
headers: {
|
|
2965
|
+
Accept: "application/json"
|
|
2966
|
+
}
|
|
2967
|
+
});
|
|
1843
2968
|
} catch (error) {
|
|
1844
|
-
|
|
1845
|
-
|
|
2969
|
+
return {
|
|
2970
|
+
ok: false,
|
|
2971
|
+
status: 503,
|
|
2972
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2973
|
+
};
|
|
1846
2974
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
)
|
|
2975
|
+
let payload = null;
|
|
2976
|
+
try {
|
|
2977
|
+
payload = await response.json();
|
|
2978
|
+
} catch {
|
|
2979
|
+
if (!response.ok) {
|
|
2980
|
+
return {
|
|
2981
|
+
ok: false,
|
|
2982
|
+
status: response.status,
|
|
2983
|
+
message: `marketplace request failed (${response.status})`
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2986
|
+
return {
|
|
2987
|
+
ok: false,
|
|
2988
|
+
status: 502,
|
|
2989
|
+
message: "invalid marketplace response"
|
|
2990
|
+
};
|
|
1852
2991
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2992
|
+
if (!response.ok) {
|
|
2993
|
+
return {
|
|
2994
|
+
ok: false,
|
|
2995
|
+
status: response.status,
|
|
2996
|
+
message: readErrorMessage(payload, `marketplace request failed (${response.status})`)
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
if (!payload || typeof payload !== "object" || !("ok" in payload)) {
|
|
3000
|
+
return {
|
|
3001
|
+
ok: false,
|
|
3002
|
+
status: 502,
|
|
3003
|
+
message: "invalid marketplace response"
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
const typed = payload;
|
|
3007
|
+
if (!typed.ok) {
|
|
3008
|
+
return {
|
|
3009
|
+
ok: false,
|
|
3010
|
+
status: 502,
|
|
3011
|
+
message: readErrorMessage(payload, "marketplace response returned error")
|
|
3012
|
+
};
|
|
1858
3013
|
}
|
|
1859
|
-
setProviderApiKey({
|
|
1860
|
-
configPath,
|
|
1861
|
-
provider: providerName,
|
|
1862
|
-
accessToken,
|
|
1863
|
-
defaultApiBase: spec.defaultApiBase
|
|
1864
|
-
});
|
|
1865
3014
|
return {
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
source: "cli",
|
|
1869
|
-
expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : void 0
|
|
3015
|
+
ok: true,
|
|
3016
|
+
data: typed.data
|
|
1870
3017
|
};
|
|
1871
3018
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
3019
|
+
function sanitizeMarketplaceItem(item) {
|
|
3020
|
+
const next = { ...item };
|
|
3021
|
+
delete next.sourceType;
|
|
3022
|
+
return next;
|
|
3023
|
+
}
|
|
3024
|
+
function readLocalizedMap(value) {
|
|
3025
|
+
const localized = {};
|
|
3026
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3027
|
+
return localized;
|
|
3028
|
+
}
|
|
3029
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3030
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
3031
|
+
continue;
|
|
3032
|
+
}
|
|
3033
|
+
localized[key] = entry.trim();
|
|
3034
|
+
}
|
|
3035
|
+
return localized;
|
|
3036
|
+
}
|
|
3037
|
+
function normalizeLocaleTag(value) {
|
|
3038
|
+
return value.trim().toLowerCase().replace(/_/g, "-");
|
|
3039
|
+
}
|
|
3040
|
+
function pickLocaleFamilyValue(localized, localeFamily) {
|
|
3041
|
+
const normalizedFamily = normalizeLocaleTag(localeFamily).split("-")[0];
|
|
3042
|
+
if (!normalizedFamily) {
|
|
3043
|
+
return void 0;
|
|
3044
|
+
}
|
|
3045
|
+
let familyMatch;
|
|
3046
|
+
for (const [locale, text] of Object.entries(localized)) {
|
|
3047
|
+
const normalizedLocale = normalizeLocaleTag(locale);
|
|
3048
|
+
if (!normalizedLocale) {
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
if (normalizedLocale === normalizedFamily) {
|
|
3052
|
+
return text;
|
|
3053
|
+
}
|
|
3054
|
+
if (!familyMatch && normalizedLocale.startsWith(`${normalizedFamily}-`)) {
|
|
3055
|
+
familyMatch = text;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
return familyMatch;
|
|
3059
|
+
}
|
|
3060
|
+
function normalizeLocalizedTextMap(primaryText, localized, zhFallback) {
|
|
3061
|
+
const next = readLocalizedMap(localized);
|
|
3062
|
+
if (!next.en) {
|
|
3063
|
+
next.en = pickLocaleFamilyValue(next, "en") ?? primaryText;
|
|
3064
|
+
}
|
|
3065
|
+
if (!next.zh) {
|
|
3066
|
+
next.zh = pickLocaleFamilyValue(next, "zh") ?? (zhFallback && zhFallback.trim().length > 0 ? zhFallback.trim() : next.en);
|
|
3067
|
+
}
|
|
3068
|
+
return next;
|
|
3069
|
+
}
|
|
3070
|
+
function normalizeMarketplaceItemForUi(item) {
|
|
3071
|
+
const zhCopy = MARKETPLACE_ZH_COPY_BY_SLUG[item.slug];
|
|
3072
|
+
const next = {
|
|
3073
|
+
...item,
|
|
3074
|
+
summaryI18n: normalizeLocalizedTextMap(item.summary, item.summaryI18n, zhCopy?.summary)
|
|
1879
3075
|
};
|
|
3076
|
+
if ("description" in item && typeof item.description === "string" && item.description.trim().length > 0) {
|
|
3077
|
+
next.descriptionI18n = normalizeLocalizedTextMap(
|
|
3078
|
+
item.description,
|
|
3079
|
+
item.descriptionI18n,
|
|
3080
|
+
zhCopy?.description
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3083
|
+
return next;
|
|
1880
3084
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
var BUILTIN_CHANNEL_PLUGIN_ID_PREFIX = "builtin-channel-";
|
|
1885
|
-
var MARKETPLACE_REMOTE_PAGE_SIZE = 100;
|
|
1886
|
-
var MARKETPLACE_REMOTE_MAX_PAGES = 20;
|
|
1887
|
-
var getWorkspacePathFromConfig3 = NextclawCore.getWorkspacePathFromConfig;
|
|
1888
|
-
function createSkillsLoader(workspace) {
|
|
1889
|
-
const ctor = NextclawCore.SkillsLoader;
|
|
1890
|
-
if (!ctor) {
|
|
1891
|
-
return null;
|
|
3085
|
+
function toPositiveInt(raw, fallback) {
|
|
3086
|
+
if (!raw) {
|
|
3087
|
+
return fallback;
|
|
1892
3088
|
}
|
|
1893
|
-
|
|
3089
|
+
const parsed = Number.parseInt(raw, 10);
|
|
3090
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
3091
|
+
return fallback;
|
|
3092
|
+
}
|
|
3093
|
+
return parsed;
|
|
3094
|
+
}
|
|
3095
|
+
async function fetchAllMarketplaceItems(params) {
|
|
3096
|
+
const items = [];
|
|
3097
|
+
let sort = "relevance";
|
|
3098
|
+
let query;
|
|
3099
|
+
for (let page = 1; page <= MARKETPLACE_REMOTE_MAX_PAGES; page += 1) {
|
|
3100
|
+
const result = await fetchMarketplaceData({
|
|
3101
|
+
baseUrl: params.baseUrl,
|
|
3102
|
+
path: params.path,
|
|
3103
|
+
query: {
|
|
3104
|
+
...params.query,
|
|
3105
|
+
page: String(page),
|
|
3106
|
+
pageSize: String(MARKETPLACE_REMOTE_PAGE_SIZE)
|
|
3107
|
+
}
|
|
3108
|
+
});
|
|
3109
|
+
if (!result.ok) {
|
|
3110
|
+
return result;
|
|
3111
|
+
}
|
|
3112
|
+
const pageItems = Array.isArray(result.data.items) ? result.data.items : [];
|
|
3113
|
+
if (pageItems.length === 0) {
|
|
3114
|
+
break;
|
|
3115
|
+
}
|
|
3116
|
+
sort = result.data.sort;
|
|
3117
|
+
query = result.data.query;
|
|
3118
|
+
items.push(...pageItems);
|
|
3119
|
+
const pageSize = typeof result.data.pageSize === "number" && Number.isFinite(result.data.pageSize) && result.data.pageSize > 0 ? result.data.pageSize : MARKETPLACE_REMOTE_PAGE_SIZE;
|
|
3120
|
+
if (pageItems.length < pageSize) {
|
|
3121
|
+
break;
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
return {
|
|
3125
|
+
ok: true,
|
|
3126
|
+
data: {
|
|
3127
|
+
sort,
|
|
3128
|
+
...typeof query === "string" ? { query } : {},
|
|
3129
|
+
items
|
|
3130
|
+
}
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
async function fetchAllPluginMarketplaceItems(params) {
|
|
3134
|
+
return fetchAllMarketplaceItems({
|
|
3135
|
+
baseUrl: params.baseUrl,
|
|
3136
|
+
path: "/api/v1/plugins/items",
|
|
3137
|
+
query: params.query
|
|
3138
|
+
});
|
|
1894
3139
|
}
|
|
3140
|
+
async function fetchAllSkillMarketplaceItems(params) {
|
|
3141
|
+
return fetchAllMarketplaceItems({
|
|
3142
|
+
baseUrl: params.baseUrl,
|
|
3143
|
+
path: "/api/v1/skills/items",
|
|
3144
|
+
query: params.query
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
function sanitizeMarketplaceListItems(items) {
|
|
3148
|
+
return items.map((item) => sanitizeMarketplaceItem(item));
|
|
3149
|
+
}
|
|
3150
|
+
function sanitizeMarketplaceItemView(item) {
|
|
3151
|
+
return sanitizeMarketplaceItem(item);
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
// src/ui/router/marketplace/installed.ts
|
|
3155
|
+
import * as NextclawCore3 from "@nextclaw/core";
|
|
3156
|
+
import { buildPluginStatusReport } from "@nextclaw/openclaw-compat";
|
|
3157
|
+
|
|
3158
|
+
// src/ui/router/marketplace/spec.ts
|
|
1895
3159
|
function normalizePluginNpmSpec(rawSpec) {
|
|
1896
3160
|
const spec = rawSpec.trim();
|
|
1897
3161
|
if (!spec.startsWith("@")) {
|
|
@@ -1940,308 +3204,68 @@ function readPluginOriginPriority(origin) {
|
|
|
1940
3204
|
if (origin === "bundled") {
|
|
1941
3205
|
return 80;
|
|
1942
3206
|
}
|
|
1943
|
-
if (origin === "workspace") {
|
|
1944
|
-
return 70;
|
|
1945
|
-
}
|
|
1946
|
-
if (origin === "global") {
|
|
1947
|
-
return 60;
|
|
1948
|
-
}
|
|
1949
|
-
if (origin === "config") {
|
|
1950
|
-
return 50;
|
|
1951
|
-
}
|
|
1952
|
-
return 10;
|
|
1953
|
-
}
|
|
1954
|
-
function readInstalledPluginRecordPriority(record) {
|
|
1955
|
-
const installScore = record.installPath ? 20 : 0;
|
|
1956
|
-
const timestampScore = record.installedAt ? 10 : 0;
|
|
1957
|
-
return readPluginRuntimeStatusPriority(record.runtimeStatus) + readPluginOriginPriority(record.origin) + installScore + timestampScore;
|
|
1958
|
-
}
|
|
1959
|
-
function mergeInstalledPluginRecords(primary, secondary) {
|
|
1960
|
-
return {
|
|
1961
|
-
...primary,
|
|
1962
|
-
id: primary.id ?? secondary.id,
|
|
1963
|
-
label: primary.label ?? secondary.label,
|
|
1964
|
-
source: primary.source ?? secondary.source,
|
|
1965
|
-
installedAt: primary.installedAt ?? secondary.installedAt,
|
|
1966
|
-
enabled: primary.enabled ?? secondary.enabled,
|
|
1967
|
-
runtimeStatus: primary.runtimeStatus ?? secondary.runtimeStatus,
|
|
1968
|
-
origin: primary.origin ?? secondary.origin,
|
|
1969
|
-
installPath: primary.installPath ?? secondary.installPath
|
|
1970
|
-
};
|
|
1971
|
-
}
|
|
1972
|
-
function dedupeInstalledPluginRecordsByCanonicalSpec(records) {
|
|
1973
|
-
const deduped = /* @__PURE__ */ new Map();
|
|
1974
|
-
for (const record of records) {
|
|
1975
|
-
const canonicalSpec = normalizePluginNpmSpec(record.spec).trim();
|
|
1976
|
-
if (!canonicalSpec) {
|
|
1977
|
-
continue;
|
|
1978
|
-
}
|
|
1979
|
-
const key = canonicalSpec.toLowerCase();
|
|
1980
|
-
const normalizedRecord = { ...record, spec: canonicalSpec };
|
|
1981
|
-
const existing = deduped.get(key);
|
|
1982
|
-
if (!existing) {
|
|
1983
|
-
deduped.set(key, normalizedRecord);
|
|
1984
|
-
continue;
|
|
1985
|
-
}
|
|
1986
|
-
const normalizedScore = readInstalledPluginRecordPriority(normalizedRecord);
|
|
1987
|
-
const existingScore = readInstalledPluginRecordPriority(existing);
|
|
1988
|
-
if (normalizedScore > existingScore) {
|
|
1989
|
-
deduped.set(key, mergeInstalledPluginRecords(normalizedRecord, existing));
|
|
1990
|
-
continue;
|
|
1991
|
-
}
|
|
1992
|
-
deduped.set(key, mergeInstalledPluginRecords(existing, normalizedRecord));
|
|
1993
|
-
}
|
|
1994
|
-
return Array.from(deduped.values());
|
|
1995
|
-
}
|
|
1996
|
-
function ok(data) {
|
|
1997
|
-
return { ok: true, data };
|
|
1998
|
-
}
|
|
1999
|
-
function err(code, message, details) {
|
|
2000
|
-
return { ok: false, error: { code, message, details } };
|
|
2001
|
-
}
|
|
2002
|
-
function toIsoTime(value) {
|
|
2003
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2004
|
-
return null;
|
|
2005
|
-
}
|
|
2006
|
-
const date = new Date(value);
|
|
2007
|
-
if (Number.isNaN(date.getTime())) {
|
|
2008
|
-
return null;
|
|
2009
|
-
}
|
|
2010
|
-
return date.toISOString();
|
|
2011
|
-
}
|
|
2012
|
-
function buildCronJobView(job) {
|
|
2013
|
-
return {
|
|
2014
|
-
id: job.id,
|
|
2015
|
-
name: job.name,
|
|
2016
|
-
enabled: job.enabled,
|
|
2017
|
-
schedule: job.schedule,
|
|
2018
|
-
payload: job.payload,
|
|
2019
|
-
state: {
|
|
2020
|
-
nextRunAt: toIsoTime(job.state.nextRunAtMs),
|
|
2021
|
-
lastRunAt: toIsoTime(job.state.lastRunAtMs),
|
|
2022
|
-
lastStatus: job.state.lastStatus ?? null,
|
|
2023
|
-
lastError: job.state.lastError ?? null
|
|
2024
|
-
},
|
|
2025
|
-
createdAt: new Date(job.createdAtMs).toISOString(),
|
|
2026
|
-
updatedAt: new Date(job.updatedAtMs).toISOString(),
|
|
2027
|
-
deleteAfterRun: job.deleteAfterRun
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
|
-
function findCronJob(service, id) {
|
|
2031
|
-
const jobs = service.listJobs(true);
|
|
2032
|
-
return jobs.find((job) => job.id === id) ?? null;
|
|
2033
|
-
}
|
|
2034
|
-
async function readJson(req) {
|
|
2035
|
-
try {
|
|
2036
|
-
const data = await req.json();
|
|
2037
|
-
return { ok: true, data };
|
|
2038
|
-
} catch {
|
|
2039
|
-
return { ok: false };
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
function isRecord(value) {
|
|
2043
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2044
|
-
}
|
|
2045
|
-
function readErrorMessage(value, fallback) {
|
|
2046
|
-
if (!isRecord(value)) {
|
|
2047
|
-
return fallback;
|
|
2048
|
-
}
|
|
2049
|
-
const maybeError = value.error;
|
|
2050
|
-
if (!isRecord(maybeError)) {
|
|
2051
|
-
return fallback;
|
|
2052
|
-
}
|
|
2053
|
-
return typeof maybeError.message === "string" && maybeError.message.trim().length > 0 ? maybeError.message : fallback;
|
|
2054
|
-
}
|
|
2055
|
-
function readNonEmptyString(value) {
|
|
2056
|
-
if (typeof value !== "string") {
|
|
2057
|
-
return void 0;
|
|
2058
|
-
}
|
|
2059
|
-
const trimmed = value.trim();
|
|
2060
|
-
return trimmed || void 0;
|
|
2061
|
-
}
|
|
2062
|
-
function formatUserFacingError(error, maxChars = 320) {
|
|
2063
|
-
const raw = error instanceof Error ? error.message || error.name || "Unknown error" : String(error ?? "Unknown error");
|
|
2064
|
-
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
2065
|
-
if (!normalized) {
|
|
2066
|
-
return "Unknown error";
|
|
2067
|
-
}
|
|
2068
|
-
if (normalized.length <= maxChars) {
|
|
2069
|
-
return normalized;
|
|
2070
|
-
}
|
|
2071
|
-
return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
2072
|
-
}
|
|
2073
|
-
function normalizeSessionType2(value) {
|
|
2074
|
-
return readNonEmptyString(value)?.toLowerCase();
|
|
2075
|
-
}
|
|
2076
|
-
function resolveSessionTypeLabel(sessionType) {
|
|
2077
|
-
if (sessionType === "native") {
|
|
2078
|
-
return "Native";
|
|
2079
|
-
}
|
|
2080
|
-
if (sessionType === "codex-sdk") {
|
|
2081
|
-
return "Codex";
|
|
2082
|
-
}
|
|
2083
|
-
if (sessionType === "claude-agent-sdk") {
|
|
2084
|
-
return "Claude Code";
|
|
2085
|
-
}
|
|
2086
|
-
return sessionType;
|
|
2087
|
-
}
|
|
2088
|
-
async function buildChatSessionTypesView(chatRuntime) {
|
|
2089
|
-
if (!chatRuntime?.listSessionTypes) {
|
|
2090
|
-
return {
|
|
2091
|
-
defaultType: DEFAULT_SESSION_TYPE,
|
|
2092
|
-
options: [{ value: DEFAULT_SESSION_TYPE, label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE) }]
|
|
2093
|
-
};
|
|
2094
|
-
}
|
|
2095
|
-
const payload = await chatRuntime.listSessionTypes();
|
|
2096
|
-
const deduped = /* @__PURE__ */ new Map();
|
|
2097
|
-
for (const rawOption of payload.options ?? []) {
|
|
2098
|
-
const normalized = normalizeSessionType2(rawOption.value);
|
|
2099
|
-
if (!normalized) {
|
|
2100
|
-
continue;
|
|
2101
|
-
}
|
|
2102
|
-
deduped.set(normalized, {
|
|
2103
|
-
value: normalized,
|
|
2104
|
-
label: readNonEmptyString(rawOption.label) ?? resolveSessionTypeLabel(normalized)
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
if (!deduped.has(DEFAULT_SESSION_TYPE)) {
|
|
2108
|
-
deduped.set(DEFAULT_SESSION_TYPE, {
|
|
2109
|
-
value: DEFAULT_SESSION_TYPE,
|
|
2110
|
-
label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE)
|
|
2111
|
-
});
|
|
2112
|
-
}
|
|
2113
|
-
const defaultType = normalizeSessionType2(payload.defaultType) ?? DEFAULT_SESSION_TYPE;
|
|
2114
|
-
if (!deduped.has(defaultType)) {
|
|
2115
|
-
deduped.set(defaultType, {
|
|
2116
|
-
value: defaultType,
|
|
2117
|
-
label: resolveSessionTypeLabel(defaultType)
|
|
2118
|
-
});
|
|
2119
|
-
}
|
|
2120
|
-
const options = Array.from(deduped.values()).sort((left, right) => {
|
|
2121
|
-
if (left.value === DEFAULT_SESSION_TYPE) {
|
|
2122
|
-
return -1;
|
|
2123
|
-
}
|
|
2124
|
-
if (right.value === DEFAULT_SESSION_TYPE) {
|
|
2125
|
-
return 1;
|
|
2126
|
-
}
|
|
2127
|
-
return left.value.localeCompare(right.value);
|
|
2128
|
-
});
|
|
2129
|
-
return {
|
|
2130
|
-
defaultType,
|
|
2131
|
-
options
|
|
2132
|
-
};
|
|
2133
|
-
}
|
|
2134
|
-
function resolveAgentIdFromSessionKey(sessionKey) {
|
|
2135
|
-
const parsed = NextclawCore.parseAgentScopedSessionKey(sessionKey);
|
|
2136
|
-
const agentId = readNonEmptyString(parsed?.agentId);
|
|
2137
|
-
return agentId;
|
|
2138
|
-
}
|
|
2139
|
-
function createChatRunId() {
|
|
2140
|
-
const now = Date.now().toString(36);
|
|
2141
|
-
const rand = Math.random().toString(36).slice(2, 10);
|
|
2142
|
-
return `run-${now}-${rand}`;
|
|
2143
|
-
}
|
|
2144
|
-
function isChatRunState(value) {
|
|
2145
|
-
return value === "queued" || value === "running" || value === "completed" || value === "failed" || value === "aborted";
|
|
2146
|
-
}
|
|
2147
|
-
function readChatRunStates(value) {
|
|
2148
|
-
if (typeof value !== "string") {
|
|
2149
|
-
return void 0;
|
|
2150
|
-
}
|
|
2151
|
-
const values = value.split(",").map((item) => item.trim().toLowerCase()).filter((item) => Boolean(item) && isChatRunState(item));
|
|
2152
|
-
if (values.length === 0) {
|
|
2153
|
-
return void 0;
|
|
2154
|
-
}
|
|
2155
|
-
return Array.from(new Set(values));
|
|
2156
|
-
}
|
|
2157
|
-
function buildChatTurnView(params) {
|
|
2158
|
-
const completedAt = /* @__PURE__ */ new Date();
|
|
2159
|
-
return {
|
|
2160
|
-
reply: String(params.result.reply ?? ""),
|
|
2161
|
-
sessionKey: readNonEmptyString(params.result.sessionKey) ?? params.fallbackSessionKey,
|
|
2162
|
-
...readNonEmptyString(params.result.agentId) || params.requestedAgentId ? { agentId: readNonEmptyString(params.result.agentId) ?? params.requestedAgentId } : {},
|
|
2163
|
-
...readNonEmptyString(params.result.model) || params.requestedModel ? { model: readNonEmptyString(params.result.model) ?? params.requestedModel } : {},
|
|
2164
|
-
requestedAt: params.requestedAt.toISOString(),
|
|
2165
|
-
completedAt: completedAt.toISOString(),
|
|
2166
|
-
durationMs: Math.max(0, completedAt.getTime() - params.startedAtMs)
|
|
2167
|
-
};
|
|
2168
|
-
}
|
|
2169
|
-
function buildChatTurnViewFromRun(params) {
|
|
2170
|
-
const requestedAt = readNonEmptyString(params.run.requestedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2171
|
-
const completedAt = readNonEmptyString(params.run.completedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2172
|
-
const requestedAtMs = Date.parse(requestedAt);
|
|
2173
|
-
const completedAtMs = Date.parse(completedAt);
|
|
2174
|
-
return {
|
|
2175
|
-
reply: readNonEmptyString(params.run.reply) ?? params.fallbackReply ?? "",
|
|
2176
|
-
sessionKey: readNonEmptyString(params.run.sessionKey) ?? params.fallbackSessionKey,
|
|
2177
|
-
...readNonEmptyString(params.run.agentId) || params.fallbackAgentId ? { agentId: readNonEmptyString(params.run.agentId) ?? params.fallbackAgentId } : {},
|
|
2178
|
-
...readNonEmptyString(params.run.model) || params.fallbackModel ? { model: readNonEmptyString(params.run.model) ?? params.fallbackModel } : {},
|
|
2179
|
-
requestedAt,
|
|
2180
|
-
completedAt,
|
|
2181
|
-
durationMs: Number.isFinite(requestedAtMs) && Number.isFinite(completedAtMs) ? Math.max(0, completedAtMs - requestedAtMs) : 0
|
|
2182
|
-
};
|
|
2183
|
-
}
|
|
2184
|
-
function toSseFrame(event, data) {
|
|
2185
|
-
return `event: ${event}
|
|
2186
|
-
data: ${JSON.stringify(data)}
|
|
2187
|
-
|
|
2188
|
-
`;
|
|
3207
|
+
if (origin === "workspace") {
|
|
3208
|
+
return 70;
|
|
3209
|
+
}
|
|
3210
|
+
if (origin === "global") {
|
|
3211
|
+
return 60;
|
|
3212
|
+
}
|
|
3213
|
+
if (origin === "config") {
|
|
3214
|
+
return 50;
|
|
3215
|
+
}
|
|
3216
|
+
return 10;
|
|
2189
3217
|
}
|
|
2190
|
-
function
|
|
2191
|
-
const
|
|
2192
|
-
const
|
|
2193
|
-
|
|
2194
|
-
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
3218
|
+
function readInstalledPluginRecordPriority(record) {
|
|
3219
|
+
const installScore = record.installPath ? 20 : 0;
|
|
3220
|
+
const timestampScore = record.installedAt ? 10 : 0;
|
|
3221
|
+
return readPluginRuntimeStatusPriority(record.runtimeStatus) + readPluginOriginPriority(record.origin) + installScore + timestampScore;
|
|
2195
3222
|
}
|
|
2196
|
-
function
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
3223
|
+
function mergeInstalledPluginRecords(primary, secondary) {
|
|
3224
|
+
return {
|
|
3225
|
+
...primary,
|
|
3226
|
+
id: primary.id ?? secondary.id,
|
|
3227
|
+
label: primary.label ?? secondary.label,
|
|
3228
|
+
source: primary.source ?? secondary.source,
|
|
3229
|
+
installedAt: primary.installedAt ?? secondary.installedAt,
|
|
3230
|
+
enabled: primary.enabled ?? secondary.enabled,
|
|
3231
|
+
runtimeStatus: primary.runtimeStatus ?? secondary.runtimeStatus,
|
|
3232
|
+
origin: primary.origin ?? secondary.origin,
|
|
3233
|
+
installPath: primary.installPath ?? secondary.installPath
|
|
3234
|
+
};
|
|
2204
3235
|
}
|
|
2205
|
-
|
|
2206
|
-
const
|
|
2207
|
-
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
Accept: "application/json"
|
|
2212
|
-
}
|
|
2213
|
-
});
|
|
2214
|
-
let payload = null;
|
|
2215
|
-
try {
|
|
2216
|
-
payload = await response.json();
|
|
2217
|
-
} catch {
|
|
2218
|
-
payload = null;
|
|
3236
|
+
function dedupeInstalledPluginRecordsByCanonicalSpec(records) {
|
|
3237
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
3238
|
+
for (const record of records) {
|
|
3239
|
+
const canonicalSpec = normalizePluginNpmSpec(record.spec).trim();
|
|
3240
|
+
if (!canonicalSpec) {
|
|
3241
|
+
continue;
|
|
2219
3242
|
}
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
3243
|
+
const key = canonicalSpec.toLowerCase();
|
|
3244
|
+
const normalizedRecord = { ...record, spec: canonicalSpec };
|
|
3245
|
+
const existing = deduped.get(key);
|
|
3246
|
+
if (!existing) {
|
|
3247
|
+
deduped.set(key, normalizedRecord);
|
|
3248
|
+
continue;
|
|
2226
3249
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
};
|
|
3250
|
+
const normalizedScore = readInstalledPluginRecordPriority(normalizedRecord);
|
|
3251
|
+
const existingScore = readInstalledPluginRecordPriority(existing);
|
|
3252
|
+
if (normalizedScore > existingScore) {
|
|
3253
|
+
deduped.set(key, mergeInstalledPluginRecords(normalizedRecord, existing));
|
|
3254
|
+
continue;
|
|
2233
3255
|
}
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
3256
|
+
deduped.set(key, mergeInstalledPluginRecords(existing, normalizedRecord));
|
|
3257
|
+
}
|
|
3258
|
+
return Array.from(deduped.values());
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
// src/ui/router/marketplace/installed.ts
|
|
3262
|
+
var getWorkspacePathFromConfig3 = NextclawCore3.getWorkspacePathFromConfig;
|
|
3263
|
+
function createSkillsLoader(workspace) {
|
|
3264
|
+
const ctor = NextclawCore3.SkillsLoader;
|
|
3265
|
+
if (!ctor) {
|
|
3266
|
+
return null;
|
|
2244
3267
|
}
|
|
3268
|
+
return new ctor(workspace);
|
|
2245
3269
|
}
|
|
2246
3270
|
function collectInstalledPluginRecords(options) {
|
|
2247
3271
|
const config = loadConfigOrDefault(options.configPath);
|
|
@@ -2393,191 +3417,41 @@ function collectSkillMarketplaceInstalledView(options) {
|
|
|
2393
3417
|
type: "skill",
|
|
2394
3418
|
total: installed.records.length,
|
|
2395
3419
|
specs: installed.specs,
|
|
2396
|
-
records: installed.records
|
|
2397
|
-
};
|
|
2398
|
-
}
|
|
2399
|
-
function resolvePluginManageTargetId(options, rawTargetId, rawSpec) {
|
|
2400
|
-
const targetId = rawTargetId.trim();
|
|
2401
|
-
if (!targetId && !rawSpec) {
|
|
2402
|
-
return rawTargetId;
|
|
2403
|
-
}
|
|
2404
|
-
const normalizedTarget = targetId ? normalizePluginNpmSpec(targetId).toLowerCase() : "";
|
|
2405
|
-
const normalizedSpec = rawSpec ? normalizePluginNpmSpec(rawSpec).toLowerCase() : "";
|
|
2406
|
-
const pluginRecords = collectInstalledPluginRecords(options).records;
|
|
2407
|
-
const lowerTargetId = targetId.toLowerCase();
|
|
2408
|
-
for (const record of pluginRecords) {
|
|
2409
|
-
const recordId = record.id?.trim();
|
|
2410
|
-
if (recordId && recordId.toLowerCase() === lowerTargetId) {
|
|
2411
|
-
return recordId;
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
if (normalizedTarget) {
|
|
2415
|
-
for (const record of pluginRecords) {
|
|
2416
|
-
const normalizedRecordSpec = normalizePluginNpmSpec(record.spec).toLowerCase();
|
|
2417
|
-
if (normalizedRecordSpec === normalizedTarget && record.id && record.id.trim().length > 0) {
|
|
2418
|
-
return record.id;
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
if (normalizedSpec && normalizedSpec !== normalizedTarget) {
|
|
2423
|
-
for (const record of pluginRecords) {
|
|
2424
|
-
const normalizedRecordSpec = normalizePluginNpmSpec(record.spec).toLowerCase();
|
|
2425
|
-
if (normalizedRecordSpec === normalizedSpec && record.id && record.id.trim().length > 0) {
|
|
2426
|
-
return record.id;
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
}
|
|
2430
|
-
return targetId || rawSpec || rawTargetId;
|
|
2431
|
-
}
|
|
2432
|
-
function sanitizeMarketplaceItem(item) {
|
|
2433
|
-
const next = { ...item };
|
|
2434
|
-
delete next.metrics;
|
|
2435
|
-
return next;
|
|
2436
|
-
}
|
|
2437
|
-
var MARKETPLACE_ZH_COPY_BY_SLUG = {
|
|
2438
|
-
weather: {
|
|
2439
|
-
summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u5929\u6C14\u67E5\u8BE2\u5DE5\u4F5C\u6D41\u3002",
|
|
2440
|
-
description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u5FEB\u901F\u5929\u6C14\u67E5\u8BE2\u5DE5\u4F5C\u6D41\u3002"
|
|
2441
|
-
},
|
|
2442
|
-
summarize: {
|
|
2443
|
-
summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u7ED3\u6784\u5316\u6458\u8981\u3002",
|
|
2444
|
-
description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u6587\u4EF6\u4E0E\u957F\u6587\u672C\u7684\u6458\u8981\u5DE5\u4F5C\u6D41\u3002"
|
|
2445
|
-
},
|
|
2446
|
-
github: {
|
|
2447
|
-
summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E GitHub \u5DE5\u4F5C\u6D41\u3002",
|
|
2448
|
-
description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B Issue\u3001PR \u4E0E\u4ED3\u5E93\u76F8\u5173\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
|
|
2449
|
-
},
|
|
2450
|
-
tmux: {
|
|
2451
|
-
summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u7EC8\u7AEF/Tmux \u534F\u4F5C\u5DE5\u4F5C\u6D41\u3002",
|
|
2452
|
-
description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u57FA\u4E8E Tmux \u7684\u4EFB\u52A1\u6267\u884C\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
|
|
2453
|
-
},
|
|
2454
|
-
gog: {
|
|
2455
|
-
summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u56FE\u8C31\u5BFC\u5411\u751F\u6210\u5DE5\u4F5C\u6D41\u3002",
|
|
2456
|
-
description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u56FE\u8C31\u4E0E\u89C4\u5212\u5BFC\u5411\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
|
|
2457
|
-
},
|
|
2458
|
-
pdf: {
|
|
2459
|
-
summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E PDF \u8BFB\u53D6/\u5408\u5E76/\u62C6\u5206/OCR \u5DE5\u4F5C\u6D41\u3002",
|
|
2460
|
-
description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u8BFB\u53D6\u3001\u63D0\u53D6\u3001\u5408\u5E76\u3001\u62C6\u5206\u3001\u65CB\u8F6C\u5E76\u5BF9 PDF \u6267\u884C OCR \u5904\u7406\u3002"
|
|
2461
|
-
},
|
|
2462
|
-
docx: {
|
|
2463
|
-
summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u521B\u5EFA\u548C\u7F16\u8F91 Word \u6587\u6863\u3002",
|
|
2464
|
-
description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u521B\u5EFA\u3001\u8BFB\u53D6\u3001\u7F16\u8F91\u5E76\u91CD\u6784 .docx \u6587\u6863\u3002"
|
|
2465
|
-
},
|
|
2466
|
-
pptx: {
|
|
2467
|
-
summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u6F14\u793A\u6587\u7A3F\u64CD\u4F5C\u3002",
|
|
2468
|
-
description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u521B\u5EFA\u3001\u89E3\u6790\u3001\u7F16\u8F91\u5E76\u91CD\u7EC4 .pptx \u6F14\u793A\u6587\u7A3F\u3002"
|
|
2469
|
-
},
|
|
2470
|
-
xlsx: {
|
|
2471
|
-
summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u8868\u683C\u6587\u6863\u5DE5\u4F5C\u6D41\u3002",
|
|
2472
|
-
description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u6253\u5F00\u3001\u7F16\u8F91\u3001\u6E05\u6D17\u5E76\u8F6C\u6362 .xlsx \u4E0E .csv \u7B49\u8868\u683C\u6587\u4EF6\u3002"
|
|
2473
|
-
},
|
|
2474
|
-
bird: {
|
|
2475
|
-
summary: "OpenClaw \u793E\u533A\u6280\u80FD\uFF0C\u7528\u4E8E X/Twitter \u8BFB\u53D6/\u641C\u7D22/\u53D1\u5E03\u5DE5\u4F5C\u6D41\u3002",
|
|
2476
|
-
description: "\u4F7F\u7528 bird CLI \u5728\u4EE3\u7406\u5DE5\u4F5C\u6D41\u4E2D\u8BFB\u53D6\u7EBF\u7A0B\u3001\u641C\u7D22\u5E16\u5B50\u5E76\u8D77\u8349\u63A8\u6587/\u56DE\u590D\u3002"
|
|
2477
|
-
},
|
|
2478
|
-
"cloudflare-deploy": {
|
|
2479
|
-
summary: "OpenAI \u7CBE\u9009\u6280\u80FD\uFF0C\u7528\u4E8E\u5728 Cloudflare \u4E0A\u90E8\u7F72\u5E94\u7528\u4E0E\u57FA\u7840\u8BBE\u65BD\u3002",
|
|
2480
|
-
description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u9009\u62E9 Cloudflare \u4EA7\u54C1\u5E76\u90E8\u7F72 Workers\u3001Pages \u53CA\u76F8\u5173\u670D\u52A1\u3002"
|
|
2481
|
-
},
|
|
2482
|
-
"channel-plugin-discord": {
|
|
2483
|
-
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Discord \u6E20\u9053\u96C6\u6210\u3002",
|
|
2484
|
-
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Discord \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2485
|
-
},
|
|
2486
|
-
"channel-plugin-telegram": {
|
|
2487
|
-
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Telegram \u6E20\u9053\u96C6\u6210\u3002",
|
|
2488
|
-
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Telegram \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2489
|
-
},
|
|
2490
|
-
"channel-plugin-slack": {
|
|
2491
|
-
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Slack \u6E20\u9053\u96C6\u6210\u3002",
|
|
2492
|
-
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Slack \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2493
|
-
},
|
|
2494
|
-
"channel-plugin-wecom": {
|
|
2495
|
-
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E\u4F01\u4E1A\u5FAE\u4FE1\u6E20\u9053\u96C6\u6210\u3002",
|
|
2496
|
-
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B\u4F01\u4E1A\u5FAE\u4FE1\u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2497
|
-
},
|
|
2498
|
-
"channel-plugin-email": {
|
|
2499
|
-
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Email \u6E20\u9053\u96C6\u6210\u3002",
|
|
2500
|
-
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Email \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2501
|
-
},
|
|
2502
|
-
"channel-plugin-whatsapp": {
|
|
2503
|
-
summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E WhatsApp \u6E20\u9053\u96C6\u6210\u3002",
|
|
2504
|
-
description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B WhatsApp \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
|
|
2505
|
-
},
|
|
2506
|
-
"channel-plugin-clawbay": {
|
|
2507
|
-
summary: "Clawbay \u5B98\u65B9\u6E20\u9053\u63D2\u4EF6\uFF0C\u7528\u4E8E NextClaw \u96C6\u6210\u3002",
|
|
2508
|
-
description: "\u901A\u8FC7\u63D2\u4EF6\u8FD0\u884C\u65F6\u4E3A NextClaw \u63D0\u4F9B Clawbay \u6E20\u9053\u80FD\u529B\u3002"
|
|
2509
|
-
}
|
|
2510
|
-
};
|
|
2511
|
-
function readLocalizedMap(value) {
|
|
2512
|
-
const localized = {};
|
|
2513
|
-
if (!isRecord(value)) {
|
|
2514
|
-
return localized;
|
|
2515
|
-
}
|
|
2516
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
2517
|
-
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
2518
|
-
continue;
|
|
2519
|
-
}
|
|
2520
|
-
localized[key] = entry.trim();
|
|
2521
|
-
}
|
|
2522
|
-
return localized;
|
|
2523
|
-
}
|
|
2524
|
-
function normalizeLocaleTag(value) {
|
|
2525
|
-
return value.trim().toLowerCase().replace(/_/g, "-");
|
|
2526
|
-
}
|
|
2527
|
-
function pickLocaleFamilyValue(localized, localeFamily) {
|
|
2528
|
-
const normalizedFamily = normalizeLocaleTag(localeFamily).split("-")[0];
|
|
2529
|
-
if (!normalizedFamily) {
|
|
2530
|
-
return void 0;
|
|
2531
|
-
}
|
|
2532
|
-
let familyMatch;
|
|
2533
|
-
for (const [locale, text] of Object.entries(localized)) {
|
|
2534
|
-
const normalizedLocale = normalizeLocaleTag(locale);
|
|
2535
|
-
if (!normalizedLocale) {
|
|
2536
|
-
continue;
|
|
2537
|
-
}
|
|
2538
|
-
if (normalizedLocale === normalizedFamily) {
|
|
2539
|
-
return text;
|
|
2540
|
-
}
|
|
2541
|
-
if (!familyMatch && normalizedLocale.startsWith(`${normalizedFamily}-`)) {
|
|
2542
|
-
familyMatch = text;
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
return familyMatch;
|
|
2546
|
-
}
|
|
2547
|
-
function normalizeLocalizedTextMap(primaryText, localized, zhFallback) {
|
|
2548
|
-
const next = readLocalizedMap(localized);
|
|
2549
|
-
if (!next.en) {
|
|
2550
|
-
next.en = pickLocaleFamilyValue(next, "en") ?? primaryText;
|
|
2551
|
-
}
|
|
2552
|
-
if (!next.zh) {
|
|
2553
|
-
next.zh = pickLocaleFamilyValue(next, "zh") ?? (zhFallback && zhFallback.trim().length > 0 ? zhFallback.trim() : next.en);
|
|
2554
|
-
}
|
|
2555
|
-
return next;
|
|
2556
|
-
}
|
|
2557
|
-
function normalizeMarketplaceItemForUi(item) {
|
|
2558
|
-
const zhCopy = MARKETPLACE_ZH_COPY_BY_SLUG[item.slug];
|
|
2559
|
-
const next = {
|
|
2560
|
-
...item,
|
|
2561
|
-
summaryI18n: normalizeLocalizedTextMap(item.summary, item.summaryI18n, zhCopy?.summary)
|
|
2562
|
-
};
|
|
2563
|
-
if ("description" in item && typeof item.description === "string" && item.description.trim().length > 0) {
|
|
2564
|
-
next.descriptionI18n = normalizeLocalizedTextMap(
|
|
2565
|
-
item.description,
|
|
2566
|
-
item.descriptionI18n,
|
|
2567
|
-
zhCopy?.description
|
|
2568
|
-
);
|
|
2569
|
-
}
|
|
2570
|
-
return next;
|
|
3420
|
+
records: installed.records
|
|
3421
|
+
};
|
|
2571
3422
|
}
|
|
2572
|
-
function
|
|
2573
|
-
|
|
2574
|
-
|
|
3423
|
+
function resolvePluginManageTargetId(options, rawTargetId, rawSpec) {
|
|
3424
|
+
const targetId = rawTargetId.trim();
|
|
3425
|
+
if (!targetId && !rawSpec) {
|
|
3426
|
+
return rawTargetId;
|
|
2575
3427
|
}
|
|
2576
|
-
const
|
|
2577
|
-
|
|
2578
|
-
|
|
3428
|
+
const normalizedTarget = targetId ? normalizePluginNpmSpec(targetId).toLowerCase() : "";
|
|
3429
|
+
const normalizedSpec = rawSpec ? normalizePluginNpmSpec(rawSpec).toLowerCase() : "";
|
|
3430
|
+
const pluginRecords = collectInstalledPluginRecords(options).records;
|
|
3431
|
+
const lowerTargetId = targetId.toLowerCase();
|
|
3432
|
+
for (const record of pluginRecords) {
|
|
3433
|
+
const recordId = record.id?.trim();
|
|
3434
|
+
if (recordId && recordId.toLowerCase() === lowerTargetId) {
|
|
3435
|
+
return recordId;
|
|
3436
|
+
}
|
|
2579
3437
|
}
|
|
2580
|
-
|
|
3438
|
+
if (normalizedTarget) {
|
|
3439
|
+
for (const record of pluginRecords) {
|
|
3440
|
+
const normalizedRecordSpec = normalizePluginNpmSpec(record.spec).toLowerCase();
|
|
3441
|
+
if (normalizedRecordSpec === normalizedTarget && record.id && record.id.trim().length > 0) {
|
|
3442
|
+
return record.id;
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
if (normalizedSpec && normalizedSpec !== normalizedTarget) {
|
|
3447
|
+
for (const record of pluginRecords) {
|
|
3448
|
+
const normalizedRecordSpec = normalizePluginNpmSpec(record.spec).toLowerCase();
|
|
3449
|
+
if (normalizedRecordSpec === normalizedSpec && record.id && record.id.trim().length > 0) {
|
|
3450
|
+
return record.id;
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
return targetId || rawSpec || rawTargetId;
|
|
2581
3455
|
}
|
|
2582
3456
|
function collectKnownSkillNames(options) {
|
|
2583
3457
|
const config = loadConfigOrDefault(options.configPath);
|
|
@@ -2608,6 +3482,8 @@ function findUnsupportedSkillInstallKind(items) {
|
|
|
2608
3482
|
}
|
|
2609
3483
|
return null;
|
|
2610
3484
|
}
|
|
3485
|
+
|
|
3486
|
+
// src/ui/router/marketplace/plugin.controller.ts
|
|
2611
3487
|
async function loadPluginReadmeFromNpm(spec) {
|
|
2612
3488
|
const encodedSpec = encodeURIComponent(spec);
|
|
2613
3489
|
const registryUrl = `https://registry.npmjs.org/${encodedSpec}`;
|
|
@@ -2673,54 +3549,6 @@ async function buildPluginContentView(item) {
|
|
|
2673
3549
|
}, null, 2)
|
|
2674
3550
|
};
|
|
2675
3551
|
}
|
|
2676
|
-
async function fetchAllMarketplaceItems(params) {
|
|
2677
|
-
const allItems = [];
|
|
2678
|
-
let remotePage = 1;
|
|
2679
|
-
let remoteTotalPages = 1;
|
|
2680
|
-
let sort = "relevance";
|
|
2681
|
-
let query;
|
|
2682
|
-
while (remotePage <= remoteTotalPages && remotePage <= MARKETPLACE_REMOTE_MAX_PAGES) {
|
|
2683
|
-
const result = await fetchMarketplaceData({
|
|
2684
|
-
baseUrl: params.baseUrl,
|
|
2685
|
-
path: `/api/v1/${params.segment}/items`,
|
|
2686
|
-
query: {
|
|
2687
|
-
...params.query,
|
|
2688
|
-
page: String(remotePage),
|
|
2689
|
-
pageSize: String(MARKETPLACE_REMOTE_PAGE_SIZE)
|
|
2690
|
-
}
|
|
2691
|
-
});
|
|
2692
|
-
if (!result.ok) {
|
|
2693
|
-
return result;
|
|
2694
|
-
}
|
|
2695
|
-
allItems.push(...result.data.items);
|
|
2696
|
-
remoteTotalPages = result.data.totalPages;
|
|
2697
|
-
sort = result.data.sort;
|
|
2698
|
-
query = result.data.query;
|
|
2699
|
-
remotePage += 1;
|
|
2700
|
-
}
|
|
2701
|
-
return {
|
|
2702
|
-
ok: true,
|
|
2703
|
-
data: {
|
|
2704
|
-
sort,
|
|
2705
|
-
query,
|
|
2706
|
-
items: allItems
|
|
2707
|
-
}
|
|
2708
|
-
};
|
|
2709
|
-
}
|
|
2710
|
-
async function fetchAllPluginMarketplaceItems(params) {
|
|
2711
|
-
return await fetchAllMarketplaceItems({
|
|
2712
|
-
baseUrl: params.baseUrl,
|
|
2713
|
-
segment: "plugins",
|
|
2714
|
-
query: params.query
|
|
2715
|
-
});
|
|
2716
|
-
}
|
|
2717
|
-
async function fetchAllSkillMarketplaceItems(params) {
|
|
2718
|
-
return await fetchAllMarketplaceItems({
|
|
2719
|
-
baseUrl: params.baseUrl,
|
|
2720
|
-
segment: "skills",
|
|
2721
|
-
query: params.query
|
|
2722
|
-
});
|
|
2723
|
-
}
|
|
2724
3552
|
async function installMarketplacePlugin(params) {
|
|
2725
3553
|
const spec = typeof params.body.spec === "string" ? params.body.spec.trim() : "";
|
|
2726
3554
|
if (!spec) {
|
|
@@ -2742,33 +3570,6 @@ async function installMarketplacePlugin(params) {
|
|
|
2742
3570
|
output: result.output
|
|
2743
3571
|
};
|
|
2744
3572
|
}
|
|
2745
|
-
async function installMarketplaceSkill(params) {
|
|
2746
|
-
const spec = typeof params.body.spec === "string" ? params.body.spec.trim() : "";
|
|
2747
|
-
if (!spec) {
|
|
2748
|
-
throw new Error("INVALID_BODY:non-empty spec is required");
|
|
2749
|
-
}
|
|
2750
|
-
const installer = params.options.marketplace?.installer;
|
|
2751
|
-
if (!installer) {
|
|
2752
|
-
throw new Error("NOT_AVAILABLE:marketplace installer is not configured");
|
|
2753
|
-
}
|
|
2754
|
-
if (!installer.installSkill) {
|
|
2755
|
-
throw new Error("NOT_AVAILABLE:skill installer is not configured");
|
|
2756
|
-
}
|
|
2757
|
-
const result = await installer.installSkill({
|
|
2758
|
-
slug: spec,
|
|
2759
|
-
kind: params.body.kind,
|
|
2760
|
-
skill: params.body.skill,
|
|
2761
|
-
installPath: params.body.installPath,
|
|
2762
|
-
force: params.body.force
|
|
2763
|
-
});
|
|
2764
|
-
params.options.publish({ type: "config.updated", payload: { path: "skills" } });
|
|
2765
|
-
return {
|
|
2766
|
-
type: "skill",
|
|
2767
|
-
spec,
|
|
2768
|
-
message: result.message,
|
|
2769
|
-
output: result.output
|
|
2770
|
-
};
|
|
2771
|
-
}
|
|
2772
3573
|
async function manageMarketplacePlugin(params) {
|
|
2773
3574
|
const action = params.body.action;
|
|
2774
3575
|
const requestedTargetId = typeof params.body.id === "string" && params.body.id.trim().length > 0 ? params.body.id.trim() : typeof params.body.spec === "string" && params.body.spec.trim().length > 0 ? params.body.spec.trim() : "";
|
|
@@ -2783,197 +3584,42 @@ async function manageMarketplacePlugin(params) {
|
|
|
2783
3584
|
}
|
|
2784
3585
|
let result;
|
|
2785
3586
|
if (action === "enable") {
|
|
2786
|
-
if (!installer.enablePlugin) {
|
|
2787
|
-
throw new Error("NOT_AVAILABLE:plugin enable is not configured");
|
|
2788
|
-
}
|
|
2789
|
-
result = await installer.enablePlugin(targetId);
|
|
2790
|
-
} else if (action === "disable") {
|
|
2791
|
-
if (!installer.disablePlugin) {
|
|
2792
|
-
throw new Error("NOT_AVAILABLE:plugin disable is not configured");
|
|
2793
|
-
}
|
|
2794
|
-
result = await installer.disablePlugin(targetId);
|
|
2795
|
-
} else {
|
|
2796
|
-
if (!installer.uninstallPlugin) {
|
|
2797
|
-
throw new Error("NOT_AVAILABLE:plugin uninstall is not configured");
|
|
2798
|
-
}
|
|
2799
|
-
result = await installer.uninstallPlugin(targetId);
|
|
2800
|
-
}
|
|
2801
|
-
params.options.publish({ type: "config.updated", payload: { path: "plugins" } });
|
|
2802
|
-
return {
|
|
2803
|
-
type: "plugin",
|
|
2804
|
-
action,
|
|
2805
|
-
id: targetId,
|
|
2806
|
-
message: result.message,
|
|
2807
|
-
output: result.output
|
|
2808
|
-
};
|
|
2809
|
-
}
|
|
2810
|
-
async function manageMarketplaceSkill(params) {
|
|
2811
|
-
const action = params.body.action;
|
|
2812
|
-
const targetId = typeof params.body.id === "string" && params.body.id.trim().length > 0 ? params.body.id.trim() : typeof params.body.spec === "string" && params.body.spec.trim().length > 0 ? params.body.spec.trim() : "";
|
|
2813
|
-
if (action !== "uninstall" || !targetId) {
|
|
2814
|
-
throw new Error("INVALID_BODY:skill manage requires uninstall action and non-empty id/spec");
|
|
2815
|
-
}
|
|
2816
|
-
const installer = params.options.marketplace?.installer;
|
|
2817
|
-
if (!installer) {
|
|
2818
|
-
throw new Error("NOT_AVAILABLE:marketplace installer is not configured");
|
|
2819
|
-
}
|
|
2820
|
-
if (!installer.uninstallSkill) {
|
|
2821
|
-
throw new Error("NOT_AVAILABLE:skill uninstall is not configured");
|
|
2822
|
-
}
|
|
2823
|
-
const result = await installer.uninstallSkill(targetId);
|
|
2824
|
-
params.options.publish({ type: "config.updated", payload: { path: "skills" } });
|
|
2825
|
-
return {
|
|
2826
|
-
type: "skill",
|
|
2827
|
-
action,
|
|
2828
|
-
id: targetId,
|
|
2829
|
-
message: result.message,
|
|
2830
|
-
output: result.output
|
|
2831
|
-
};
|
|
2832
|
-
}
|
|
2833
|
-
function registerPluginMarketplaceRoutes(app, options, marketplaceBaseUrl) {
|
|
2834
|
-
app.get("/api/marketplace/plugins/installed", (c) => {
|
|
2835
|
-
return c.json(ok(collectPluginMarketplaceInstalledView(options)));
|
|
2836
|
-
});
|
|
2837
|
-
app.get("/api/marketplace/plugins/items", async (c) => {
|
|
2838
|
-
const query = c.req.query();
|
|
2839
|
-
const result = await fetchAllPluginMarketplaceItems({
|
|
2840
|
-
baseUrl: marketplaceBaseUrl,
|
|
2841
|
-
query: {
|
|
2842
|
-
q: query.q,
|
|
2843
|
-
tag: query.tag,
|
|
2844
|
-
sort: query.sort,
|
|
2845
|
-
page: query.page,
|
|
2846
|
-
pageSize: query.pageSize
|
|
2847
|
-
}
|
|
2848
|
-
});
|
|
2849
|
-
if (!result.ok) {
|
|
2850
|
-
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
2851
|
-
}
|
|
2852
|
-
const filteredItems = result.data.items.map((item) => normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(item))).filter((item) => isSupportedMarketplacePluginItem(item));
|
|
2853
|
-
const pageSize = Math.min(100, toPositiveInt(query.pageSize, 20));
|
|
2854
|
-
const requestedPage = toPositiveInt(query.page, 1);
|
|
2855
|
-
const totalPages = filteredItems.length === 0 ? 0 : Math.ceil(filteredItems.length / pageSize);
|
|
2856
|
-
const currentPage = totalPages === 0 ? 1 : Math.min(requestedPage, totalPages);
|
|
2857
|
-
return c.json(ok({
|
|
2858
|
-
total: filteredItems.length,
|
|
2859
|
-
page: currentPage,
|
|
2860
|
-
pageSize,
|
|
2861
|
-
totalPages,
|
|
2862
|
-
sort: result.data.sort,
|
|
2863
|
-
query: result.data.query,
|
|
2864
|
-
items: filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
|
2865
|
-
}));
|
|
2866
|
-
});
|
|
2867
|
-
app.get("/api/marketplace/plugins/items/:slug", async (c) => {
|
|
2868
|
-
const slug = encodeURIComponent(c.req.param("slug"));
|
|
2869
|
-
const result = await fetchMarketplaceData({
|
|
2870
|
-
baseUrl: marketplaceBaseUrl,
|
|
2871
|
-
path: `/api/v1/plugins/items/${slug}`
|
|
2872
|
-
});
|
|
2873
|
-
if (!result.ok) {
|
|
2874
|
-
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
2875
|
-
}
|
|
2876
|
-
const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
|
|
2877
|
-
if (!isSupportedMarketplacePluginItem(sanitized)) {
|
|
2878
|
-
return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
|
|
2879
|
-
}
|
|
2880
|
-
return c.json(ok(sanitized));
|
|
2881
|
-
});
|
|
2882
|
-
app.get("/api/marketplace/plugins/items/:slug/content", async (c) => {
|
|
2883
|
-
const slug = encodeURIComponent(c.req.param("slug"));
|
|
2884
|
-
const result = await fetchMarketplaceData({
|
|
2885
|
-
baseUrl: marketplaceBaseUrl,
|
|
2886
|
-
path: `/api/v1/plugins/items/${slug}`
|
|
2887
|
-
});
|
|
2888
|
-
if (!result.ok) {
|
|
2889
|
-
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
2890
|
-
}
|
|
2891
|
-
const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
|
|
2892
|
-
if (!isSupportedMarketplacePluginItem(sanitized)) {
|
|
2893
|
-
return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
|
|
2894
|
-
}
|
|
2895
|
-
const content = await buildPluginContentView(sanitized);
|
|
2896
|
-
return c.json(ok(content));
|
|
2897
|
-
});
|
|
2898
|
-
app.post("/api/marketplace/plugins/install", async (c) => {
|
|
2899
|
-
const body = await readJson(c.req.raw);
|
|
2900
|
-
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
2901
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2902
|
-
}
|
|
2903
|
-
if (body.data.type && body.data.type !== "plugin") {
|
|
2904
|
-
return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
|
|
2905
|
-
}
|
|
2906
|
-
try {
|
|
2907
|
-
const payload = await installMarketplacePlugin({
|
|
2908
|
-
options,
|
|
2909
|
-
body: body.data
|
|
2910
|
-
});
|
|
2911
|
-
return c.json(ok(payload));
|
|
2912
|
-
} catch (error) {
|
|
2913
|
-
const message = String(error);
|
|
2914
|
-
if (message.startsWith("INVALID_BODY:")) {
|
|
2915
|
-
return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
|
|
2916
|
-
}
|
|
2917
|
-
if (message.startsWith("NOT_AVAILABLE:")) {
|
|
2918
|
-
return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
|
|
2919
|
-
}
|
|
2920
|
-
return c.json(err("INSTALL_FAILED", message), 400);
|
|
2921
|
-
}
|
|
2922
|
-
});
|
|
2923
|
-
app.post("/api/marketplace/plugins/manage", async (c) => {
|
|
2924
|
-
const body = await readJson(c.req.raw);
|
|
2925
|
-
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
2926
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2927
|
-
}
|
|
2928
|
-
if (body.data.type && body.data.type !== "plugin") {
|
|
2929
|
-
return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
|
|
2930
|
-
}
|
|
2931
|
-
try {
|
|
2932
|
-
const payload = await manageMarketplacePlugin({
|
|
2933
|
-
options,
|
|
2934
|
-
body: body.data
|
|
2935
|
-
});
|
|
2936
|
-
return c.json(ok(payload));
|
|
2937
|
-
} catch (error) {
|
|
2938
|
-
const message = String(error);
|
|
2939
|
-
if (message.startsWith("INVALID_BODY:")) {
|
|
2940
|
-
return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
|
|
2941
|
-
}
|
|
2942
|
-
if (message.startsWith("NOT_AVAILABLE:")) {
|
|
2943
|
-
return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
|
|
2944
|
-
}
|
|
2945
|
-
return c.json(err("MANAGE_FAILED", message), 400);
|
|
2946
|
-
}
|
|
2947
|
-
});
|
|
2948
|
-
app.get("/api/marketplace/plugins/recommendations", async (c) => {
|
|
2949
|
-
const query = c.req.query();
|
|
2950
|
-
const result = await fetchMarketplaceData({
|
|
2951
|
-
baseUrl: marketplaceBaseUrl,
|
|
2952
|
-
path: "/api/v1/plugins/recommendations",
|
|
2953
|
-
query: {
|
|
2954
|
-
scene: query.scene,
|
|
2955
|
-
limit: query.limit
|
|
2956
|
-
}
|
|
2957
|
-
});
|
|
2958
|
-
if (!result.ok) {
|
|
2959
|
-
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3587
|
+
if (!installer.enablePlugin) {
|
|
3588
|
+
throw new Error("NOT_AVAILABLE:plugin enable is not configured");
|
|
2960
3589
|
}
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
}
|
|
3590
|
+
result = await installer.enablePlugin(targetId);
|
|
3591
|
+
} else if (action === "disable") {
|
|
3592
|
+
if (!installer.disablePlugin) {
|
|
3593
|
+
throw new Error("NOT_AVAILABLE:plugin disable is not configured");
|
|
3594
|
+
}
|
|
3595
|
+
result = await installer.disablePlugin(targetId);
|
|
3596
|
+
} else {
|
|
3597
|
+
if (!installer.uninstallPlugin) {
|
|
3598
|
+
throw new Error("NOT_AVAILABLE:plugin uninstall is not configured");
|
|
3599
|
+
}
|
|
3600
|
+
result = await installer.uninstallPlugin(targetId);
|
|
3601
|
+
}
|
|
3602
|
+
params.options.publish({ type: "config.updated", payload: { path: "plugins" } });
|
|
3603
|
+
return {
|
|
3604
|
+
type: "plugin",
|
|
3605
|
+
action,
|
|
3606
|
+
id: targetId,
|
|
3607
|
+
message: result.message,
|
|
3608
|
+
output: result.output
|
|
3609
|
+
};
|
|
2968
3610
|
}
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
3611
|
+
var PluginMarketplaceController = class {
|
|
3612
|
+
constructor(options, marketplaceBaseUrl) {
|
|
3613
|
+
this.options = options;
|
|
3614
|
+
this.marketplaceBaseUrl = marketplaceBaseUrl;
|
|
3615
|
+
}
|
|
3616
|
+
getInstalled = (c) => {
|
|
3617
|
+
return c.json(ok(collectPluginMarketplaceInstalledView(this.options)));
|
|
3618
|
+
};
|
|
3619
|
+
listItems = async (c) => {
|
|
2974
3620
|
const query = c.req.query();
|
|
2975
|
-
const result = await
|
|
2976
|
-
baseUrl: marketplaceBaseUrl,
|
|
3621
|
+
const result = await fetchAllPluginMarketplaceItems({
|
|
3622
|
+
baseUrl: this.marketplaceBaseUrl,
|
|
2977
3623
|
query: {
|
|
2978
3624
|
q: query.q,
|
|
2979
3625
|
tag: query.tag,
|
|
@@ -2985,16 +3631,7 @@ function registerSkillMarketplaceRoutes(app, options, marketplaceBaseUrl) {
|
|
|
2985
3631
|
if (!result.ok) {
|
|
2986
3632
|
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
2987
3633
|
}
|
|
2988
|
-
const
|
|
2989
|
-
const unsupportedKind = findUnsupportedSkillInstallKind(normalizedItems);
|
|
2990
|
-
if (unsupportedKind) {
|
|
2991
|
-
return c.json(
|
|
2992
|
-
err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
|
|
2993
|
-
502
|
|
2994
|
-
);
|
|
2995
|
-
}
|
|
2996
|
-
const knownSkillNames = collectKnownSkillNames(options);
|
|
2997
|
-
const filteredItems = normalizedItems.filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
|
|
3634
|
+
const filteredItems = sanitizeMarketplaceListItems(result.data.items).map((item) => normalizeMarketplaceItemForUi(item)).filter((item) => isSupportedMarketplacePluginItem(item));
|
|
2998
3635
|
const pageSize = Math.min(100, toPositiveInt(query.pageSize, 20));
|
|
2999
3636
|
const requestedPage = toPositiveInt(query.page, 1);
|
|
3000
3637
|
const totalPages = filteredItems.length === 0 ? 0 : Math.ceil(filteredItems.length / pageSize);
|
|
@@ -3008,825 +3645,362 @@ function registerSkillMarketplaceRoutes(app, options, marketplaceBaseUrl) {
|
|
|
3008
3645
|
query: result.data.query,
|
|
3009
3646
|
items: filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
|
3010
3647
|
}));
|
|
3011
|
-
}
|
|
3012
|
-
|
|
3013
|
-
const slug = encodeURIComponent(c.req.param("slug"));
|
|
3014
|
-
const result = await fetchMarketplaceData({
|
|
3015
|
-
baseUrl: marketplaceBaseUrl,
|
|
3016
|
-
path: `/api/v1/skills/items/${slug}`
|
|
3017
|
-
});
|
|
3018
|
-
if (!result.ok) {
|
|
3019
|
-
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3020
|
-
}
|
|
3021
|
-
const knownSkillNames = collectKnownSkillNames(options);
|
|
3022
|
-
const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
|
|
3023
|
-
const unsupportedKind = findUnsupportedSkillInstallKind([sanitized]);
|
|
3024
|
-
if (unsupportedKind) {
|
|
3025
|
-
return c.json(
|
|
3026
|
-
err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
|
|
3027
|
-
502
|
|
3028
|
-
);
|
|
3029
|
-
}
|
|
3030
|
-
if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) {
|
|
3031
|
-
return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
|
|
3032
|
-
}
|
|
3033
|
-
return c.json(ok(sanitized));
|
|
3034
|
-
});
|
|
3035
|
-
app.get("/api/marketplace/skills/items/:slug/content", async (c) => {
|
|
3648
|
+
};
|
|
3649
|
+
getItem = async (c) => {
|
|
3036
3650
|
const slug = encodeURIComponent(c.req.param("slug"));
|
|
3037
3651
|
const result = await fetchMarketplaceData({
|
|
3038
|
-
baseUrl: marketplaceBaseUrl,
|
|
3039
|
-
path: `/api/v1/
|
|
3040
|
-
});
|
|
3041
|
-
if (!result.ok) {
|
|
3042
|
-
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3043
|
-
}
|
|
3044
|
-
const knownSkillNames = collectKnownSkillNames(options);
|
|
3045
|
-
const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
|
|
3046
|
-
const unsupportedKind = findUnsupportedSkillInstallKind([sanitized]);
|
|
3047
|
-
if (unsupportedKind) {
|
|
3048
|
-
return c.json(
|
|
3049
|
-
err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
|
|
3050
|
-
502
|
|
3051
|
-
);
|
|
3052
|
-
}
|
|
3053
|
-
if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) {
|
|
3054
|
-
return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
|
|
3055
|
-
}
|
|
3056
|
-
const contentResult = await fetchMarketplaceData({
|
|
3057
|
-
baseUrl: marketplaceBaseUrl,
|
|
3058
|
-
path: `/api/v1/skills/items/${slug}/content`
|
|
3059
|
-
});
|
|
3060
|
-
if (!contentResult.ok) {
|
|
3061
|
-
return c.json(err("MARKETPLACE_UNAVAILABLE", contentResult.message), contentResult.status);
|
|
3062
|
-
}
|
|
3063
|
-
return c.json(ok(contentResult.data));
|
|
3064
|
-
});
|
|
3065
|
-
app.post("/api/marketplace/skills/install", async (c) => {
|
|
3066
|
-
const body = await readJson(c.req.raw);
|
|
3067
|
-
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
3068
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3069
|
-
}
|
|
3070
|
-
if (body.data.type && body.data.type !== "skill") {
|
|
3071
|
-
return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
|
|
3072
|
-
}
|
|
3073
|
-
try {
|
|
3074
|
-
const payload = await installMarketplaceSkill({
|
|
3075
|
-
options,
|
|
3076
|
-
body: body.data
|
|
3077
|
-
});
|
|
3078
|
-
return c.json(ok(payload));
|
|
3079
|
-
} catch (error) {
|
|
3080
|
-
const message = String(error);
|
|
3081
|
-
if (message.startsWith("INVALID_BODY:")) {
|
|
3082
|
-
return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
|
|
3083
|
-
}
|
|
3084
|
-
if (message.startsWith("NOT_AVAILABLE:")) {
|
|
3085
|
-
return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
|
|
3086
|
-
}
|
|
3087
|
-
return c.json(err("INSTALL_FAILED", message), 400);
|
|
3088
|
-
}
|
|
3089
|
-
});
|
|
3090
|
-
app.post("/api/marketplace/skills/manage", async (c) => {
|
|
3091
|
-
const body = await readJson(c.req.raw);
|
|
3092
|
-
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
3093
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3094
|
-
}
|
|
3095
|
-
if (body.data.type && body.data.type !== "skill") {
|
|
3096
|
-
return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
|
|
3097
|
-
}
|
|
3098
|
-
try {
|
|
3099
|
-
const payload = await manageMarketplaceSkill({
|
|
3100
|
-
options,
|
|
3101
|
-
body: body.data
|
|
3102
|
-
});
|
|
3103
|
-
return c.json(ok(payload));
|
|
3104
|
-
} catch (error) {
|
|
3105
|
-
const message = String(error);
|
|
3106
|
-
if (message.startsWith("INVALID_BODY:")) {
|
|
3107
|
-
return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
|
|
3108
|
-
}
|
|
3109
|
-
if (message.startsWith("NOT_AVAILABLE:")) {
|
|
3110
|
-
return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
|
|
3111
|
-
}
|
|
3112
|
-
return c.json(err("MANAGE_FAILED", message), 400);
|
|
3113
|
-
}
|
|
3114
|
-
});
|
|
3115
|
-
app.get("/api/marketplace/skills/recommendations", async (c) => {
|
|
3116
|
-
const query = c.req.query();
|
|
3117
|
-
const result = await fetchMarketplaceData({
|
|
3118
|
-
baseUrl: marketplaceBaseUrl,
|
|
3119
|
-
path: "/api/v1/skills/recommendations",
|
|
3120
|
-
query: {
|
|
3121
|
-
scene: query.scene,
|
|
3122
|
-
limit: query.limit
|
|
3123
|
-
}
|
|
3652
|
+
baseUrl: this.marketplaceBaseUrl,
|
|
3653
|
+
path: `/api/v1/plugins/items/${slug}`
|
|
3124
3654
|
});
|
|
3125
3655
|
if (!result.ok) {
|
|
3126
|
-
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3127
|
-
}
|
|
3128
|
-
const knownSkillNames = collectKnownSkillNames(options);
|
|
3129
|
-
const filteredItems = result.data.items.map((item) => normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(item))).filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
|
|
3130
|
-
return c.json(ok({
|
|
3131
|
-
...result.data,
|
|
3132
|
-
total: filteredItems.length,
|
|
3133
|
-
items: filteredItems
|
|
3134
|
-
}));
|
|
3135
|
-
});
|
|
3136
|
-
}
|
|
3137
|
-
function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
|
|
3138
|
-
registerPluginMarketplaceRoutes(app, options, marketplaceBaseUrl);
|
|
3139
|
-
registerSkillMarketplaceRoutes(app, options, marketplaceBaseUrl);
|
|
3140
|
-
}
|
|
3141
|
-
function createUiRouter(options) {
|
|
3142
|
-
const app = new Hono();
|
|
3143
|
-
const marketplaceBaseUrl = normalizeMarketplaceBaseUrl(options);
|
|
3144
|
-
app.notFound((c) => c.json(err("NOT_FOUND", "endpoint not found"), 404));
|
|
3145
|
-
app.get(
|
|
3146
|
-
"/api/health",
|
|
3147
|
-
(c) => c.json(
|
|
3148
|
-
ok({
|
|
3149
|
-
status: "ok",
|
|
3150
|
-
services: {
|
|
3151
|
-
chatRuntime: options.chatRuntime ? "ready" : "unavailable",
|
|
3152
|
-
cronService: options.cronService ? "ready" : "unavailable"
|
|
3153
|
-
}
|
|
3154
|
-
})
|
|
3155
|
-
)
|
|
3156
|
-
);
|
|
3157
|
-
app.get("/api/app/meta", (c) => c.json(ok(buildAppMetaView(options))));
|
|
3158
|
-
app.get("/api/config", (c) => {
|
|
3159
|
-
const config = loadConfigOrDefault(options.configPath);
|
|
3160
|
-
return c.json(ok(buildConfigView(config)));
|
|
3161
|
-
});
|
|
3162
|
-
app.get("/api/config/meta", (c) => {
|
|
3163
|
-
const config = loadConfigOrDefault(options.configPath);
|
|
3164
|
-
return c.json(ok(buildConfigMeta(config)));
|
|
3165
|
-
});
|
|
3166
|
-
app.get("/api/config/schema", (c) => {
|
|
3167
|
-
const config = loadConfigOrDefault(options.configPath);
|
|
3168
|
-
return c.json(ok(buildConfigSchemaView(config)));
|
|
3169
|
-
});
|
|
3170
|
-
app.put("/api/config/model", async (c) => {
|
|
3171
|
-
const body = await readJson(c.req.raw);
|
|
3172
|
-
if (!body.ok) {
|
|
3173
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3174
|
-
}
|
|
3175
|
-
const hasModel = typeof body.data.model === "string";
|
|
3176
|
-
if (!hasModel) {
|
|
3177
|
-
return c.json(err("INVALID_BODY", "model is required"), 400);
|
|
3178
|
-
}
|
|
3179
|
-
const view = updateModel(options.configPath, {
|
|
3180
|
-
model: body.data.model
|
|
3181
|
-
});
|
|
3182
|
-
if (hasModel) {
|
|
3183
|
-
options.publish({ type: "config.updated", payload: { path: "agents.defaults.model" } });
|
|
3184
|
-
}
|
|
3185
|
-
return c.json(ok({
|
|
3186
|
-
model: view.agents.defaults.model
|
|
3187
|
-
}));
|
|
3188
|
-
});
|
|
3189
|
-
app.put("/api/config/search", async (c) => {
|
|
3190
|
-
const body = await readJson(c.req.raw);
|
|
3191
|
-
if (!body.ok) {
|
|
3192
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3193
|
-
}
|
|
3194
|
-
const result = updateSearch(options.configPath, body.data);
|
|
3195
|
-
options.publish({ type: "config.updated", payload: { path: "search" } });
|
|
3196
|
-
return c.json(ok(result));
|
|
3197
|
-
});
|
|
3198
|
-
app.put("/api/config/providers/:provider", async (c) => {
|
|
3199
|
-
const provider = c.req.param("provider");
|
|
3200
|
-
const body = await readJson(c.req.raw);
|
|
3201
|
-
if (!body.ok) {
|
|
3202
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3203
|
-
}
|
|
3204
|
-
const result = updateProvider(options.configPath, provider, body.data);
|
|
3205
|
-
if (!result) {
|
|
3206
|
-
return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
|
|
3207
|
-
}
|
|
3208
|
-
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
3209
|
-
return c.json(ok(result));
|
|
3210
|
-
});
|
|
3211
|
-
app.post("/api/config/providers", async (c) => {
|
|
3212
|
-
const body = await readJson(c.req.raw);
|
|
3213
|
-
if (!body.ok) {
|
|
3214
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3215
|
-
}
|
|
3216
|
-
const result = createCustomProvider(
|
|
3217
|
-
options.configPath,
|
|
3218
|
-
body.data
|
|
3219
|
-
);
|
|
3220
|
-
options.publish({ type: "config.updated", payload: { path: `providers.${result.name}` } });
|
|
3221
|
-
return c.json(ok({
|
|
3222
|
-
name: result.name,
|
|
3223
|
-
provider: result.provider
|
|
3224
|
-
}));
|
|
3225
|
-
});
|
|
3226
|
-
app.delete("/api/config/providers/:provider", async (c) => {
|
|
3227
|
-
const provider = c.req.param("provider");
|
|
3228
|
-
const result = deleteCustomProvider(options.configPath, provider);
|
|
3229
|
-
if (result === null) {
|
|
3230
|
-
return c.json(err("NOT_FOUND", `custom provider not found: ${provider}`), 404);
|
|
3231
|
-
}
|
|
3232
|
-
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
3233
|
-
return c.json(ok({
|
|
3234
|
-
deleted: true,
|
|
3235
|
-
provider
|
|
3236
|
-
}));
|
|
3237
|
-
});
|
|
3238
|
-
app.post("/api/config/providers/:provider/test", async (c) => {
|
|
3239
|
-
const provider = c.req.param("provider");
|
|
3240
|
-
const body = await readJson(c.req.raw);
|
|
3241
|
-
if (!body.ok) {
|
|
3242
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3243
|
-
}
|
|
3244
|
-
const result = await testProviderConnection(
|
|
3245
|
-
options.configPath,
|
|
3246
|
-
provider,
|
|
3247
|
-
body.data
|
|
3248
|
-
);
|
|
3249
|
-
if (!result) {
|
|
3250
|
-
return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
|
|
3251
|
-
}
|
|
3252
|
-
return c.json(ok(result));
|
|
3253
|
-
});
|
|
3254
|
-
app.post("/api/config/providers/:provider/auth/start", async (c) => {
|
|
3255
|
-
const provider = c.req.param("provider");
|
|
3256
|
-
let payload = {};
|
|
3257
|
-
const rawBody = await c.req.raw.text();
|
|
3258
|
-
if (rawBody.trim().length > 0) {
|
|
3259
|
-
try {
|
|
3260
|
-
payload = JSON.parse(rawBody);
|
|
3261
|
-
} catch {
|
|
3262
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3263
|
-
}
|
|
3264
|
-
}
|
|
3265
|
-
const methodId = typeof payload.methodId === "string" ? payload.methodId.trim() : void 0;
|
|
3266
|
-
try {
|
|
3267
|
-
const result = await startProviderAuth(options.configPath, provider, {
|
|
3268
|
-
methodId
|
|
3269
|
-
});
|
|
3270
|
-
if (!result) {
|
|
3271
|
-
return c.json(err("NOT_SUPPORTED", `provider auth is not supported: ${provider}`), 404);
|
|
3272
|
-
}
|
|
3273
|
-
return c.json(ok(result));
|
|
3274
|
-
} catch (error) {
|
|
3275
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3276
|
-
return c.json(err("AUTH_START_FAILED", message), 400);
|
|
3277
|
-
}
|
|
3278
|
-
});
|
|
3279
|
-
app.post("/api/config/providers/:provider/auth/poll", async (c) => {
|
|
3280
|
-
const provider = c.req.param("provider");
|
|
3281
|
-
const body = await readJson(c.req.raw);
|
|
3282
|
-
if (!body.ok) {
|
|
3283
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3656
|
+
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3284
3657
|
}
|
|
3285
|
-
const
|
|
3286
|
-
if (!
|
|
3287
|
-
return c.json(err("
|
|
3658
|
+
const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItemView(result.data));
|
|
3659
|
+
if (!isSupportedMarketplacePluginItem(sanitized)) {
|
|
3660
|
+
return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
|
|
3288
3661
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3662
|
+
return c.json(ok(sanitized));
|
|
3663
|
+
};
|
|
3664
|
+
getItemContent = async (c) => {
|
|
3665
|
+
const slug = encodeURIComponent(c.req.param("slug"));
|
|
3666
|
+
const result = await fetchMarketplaceData({
|
|
3667
|
+
baseUrl: this.marketplaceBaseUrl,
|
|
3668
|
+
path: `/api/v1/plugins/items/${slug}`
|
|
3293
3669
|
});
|
|
3294
|
-
if (!result) {
|
|
3295
|
-
return c.json(err("
|
|
3296
|
-
}
|
|
3297
|
-
if (result.status === "authorized") {
|
|
3298
|
-
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
3670
|
+
if (!result.ok) {
|
|
3671
|
+
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3299
3672
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
const provider = c.req.param("provider");
|
|
3304
|
-
try {
|
|
3305
|
-
const result = await importProviderAuthFromCli(options.configPath, provider);
|
|
3306
|
-
if (!result) {
|
|
3307
|
-
return c.json(err("NOT_SUPPORTED", `provider cli auth import is not supported: ${provider}`), 404);
|
|
3308
|
-
}
|
|
3309
|
-
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
3310
|
-
return c.json(ok(result));
|
|
3311
|
-
} catch (error) {
|
|
3312
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3313
|
-
return c.json(err("AUTH_IMPORT_FAILED", message), 400);
|
|
3673
|
+
const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItemView(result.data));
|
|
3674
|
+
if (!isSupportedMarketplacePluginItem(sanitized)) {
|
|
3675
|
+
return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
|
|
3314
3676
|
}
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3677
|
+
const content = await buildPluginContentView(sanitized);
|
|
3678
|
+
return c.json(ok(content));
|
|
3679
|
+
};
|
|
3680
|
+
install = async (c) => {
|
|
3318
3681
|
const body = await readJson(c.req.raw);
|
|
3319
|
-
if (!body.ok) {
|
|
3682
|
+
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
3320
3683
|
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3321
3684
|
}
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
return c.json(err("NOT_FOUND", `unknown channel: ${channel}`), 404);
|
|
3685
|
+
if (body.data.type && body.data.type !== "plugin") {
|
|
3686
|
+
return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
|
|
3325
3687
|
}
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3688
|
+
try {
|
|
3689
|
+
const payload = await installMarketplacePlugin({
|
|
3690
|
+
options: this.options,
|
|
3691
|
+
body: body.data
|
|
3692
|
+
});
|
|
3693
|
+
return c.json(ok(payload));
|
|
3694
|
+
} catch (error) {
|
|
3695
|
+
const message = String(error);
|
|
3696
|
+
if (message.startsWith("INVALID_BODY:")) {
|
|
3697
|
+
return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
|
|
3698
|
+
}
|
|
3699
|
+
if (message.startsWith("NOT_AVAILABLE:")) {
|
|
3700
|
+
return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
|
|
3701
|
+
}
|
|
3702
|
+
return c.json(err("INSTALL_FAILED", message), 400);
|
|
3703
|
+
}
|
|
3704
|
+
};
|
|
3705
|
+
manage = async (c) => {
|
|
3330
3706
|
const body = await readJson(c.req.raw);
|
|
3331
|
-
if (!body.ok) {
|
|
3707
|
+
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
3332
3708
|
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3333
3709
|
}
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
return c.json(ok(result));
|
|
3337
|
-
});
|
|
3338
|
-
app.get("/api/chat/capabilities", async (c) => {
|
|
3339
|
-
const chatRuntime = options.chatRuntime;
|
|
3340
|
-
if (!chatRuntime) {
|
|
3341
|
-
return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
|
|
3342
|
-
}
|
|
3343
|
-
const query = c.req.query();
|
|
3344
|
-
const params = {
|
|
3345
|
-
sessionKey: readNonEmptyString(query.sessionKey),
|
|
3346
|
-
agentId: readNonEmptyString(query.agentId)
|
|
3347
|
-
};
|
|
3348
|
-
try {
|
|
3349
|
-
const capabilities = chatRuntime.getCapabilities ? await chatRuntime.getCapabilities(params) : { stopSupported: Boolean(chatRuntime.stopTurn) };
|
|
3350
|
-
return c.json(ok(capabilities));
|
|
3351
|
-
} catch (error) {
|
|
3352
|
-
return c.json(err("CHAT_RUNTIME_FAILED", String(error)), 500);
|
|
3710
|
+
if (body.data.type && body.data.type !== "plugin") {
|
|
3711
|
+
return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
|
|
3353
3712
|
}
|
|
3354
|
-
});
|
|
3355
|
-
app.get("/api/chat/session-types", async (c) => {
|
|
3356
3713
|
try {
|
|
3357
|
-
const payload = await
|
|
3714
|
+
const payload = await manageMarketplacePlugin({
|
|
3715
|
+
options: this.options,
|
|
3716
|
+
body: body.data
|
|
3717
|
+
});
|
|
3358
3718
|
return c.json(ok(payload));
|
|
3359
3719
|
} catch (error) {
|
|
3360
|
-
|
|
3720
|
+
const message = String(error);
|
|
3721
|
+
if (message.startsWith("INVALID_BODY:")) {
|
|
3722
|
+
return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
|
|
3723
|
+
}
|
|
3724
|
+
if (message.startsWith("NOT_AVAILABLE:")) {
|
|
3725
|
+
return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
|
|
3726
|
+
}
|
|
3727
|
+
return c.json(err("MANAGE_FAILED", message), 400);
|
|
3361
3728
|
}
|
|
3362
|
-
}
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
...option.required === true ? { required: true } : {}
|
|
3376
|
-
}))
|
|
3377
|
-
} : {}
|
|
3378
|
-
}));
|
|
3379
|
-
const payload = {
|
|
3380
|
-
commands,
|
|
3381
|
-
total: commands.length
|
|
3382
|
-
};
|
|
3383
|
-
return c.json(ok(payload));
|
|
3384
|
-
} catch (error) {
|
|
3385
|
-
return c.json(err("CHAT_COMMANDS_FAILED", String(error)), 500);
|
|
3729
|
+
};
|
|
3730
|
+
getRecommendations = async (c) => {
|
|
3731
|
+
const query = c.req.query();
|
|
3732
|
+
const result = await fetchMarketplaceData({
|
|
3733
|
+
baseUrl: this.marketplaceBaseUrl,
|
|
3734
|
+
path: "/api/v1/plugins/recommendations",
|
|
3735
|
+
query: {
|
|
3736
|
+
scene: query.scene,
|
|
3737
|
+
limit: query.limit
|
|
3738
|
+
}
|
|
3739
|
+
});
|
|
3740
|
+
if (!result.ok) {
|
|
3741
|
+
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3386
3742
|
}
|
|
3743
|
+
const filteredItems = sanitizeMarketplaceListItems(result.data.items).map((item) => normalizeMarketplaceItemForUi(item)).filter((item) => isSupportedMarketplacePluginItem(item));
|
|
3744
|
+
return c.json(ok({
|
|
3745
|
+
...result.data,
|
|
3746
|
+
total: filteredItems.length,
|
|
3747
|
+
items: filteredItems
|
|
3748
|
+
}));
|
|
3749
|
+
};
|
|
3750
|
+
};
|
|
3751
|
+
|
|
3752
|
+
// src/ui/router/marketplace/skill.controller.ts
|
|
3753
|
+
async function installMarketplaceSkill(params) {
|
|
3754
|
+
const spec = typeof params.body.spec === "string" ? params.body.spec.trim() : "";
|
|
3755
|
+
if (!spec) {
|
|
3756
|
+
throw new Error("INVALID_BODY:non-empty spec is required");
|
|
3757
|
+
}
|
|
3758
|
+
const installer = params.options.marketplace?.installer;
|
|
3759
|
+
if (!installer) {
|
|
3760
|
+
throw new Error("NOT_AVAILABLE:marketplace installer is not configured");
|
|
3761
|
+
}
|
|
3762
|
+
if (!installer.installSkill) {
|
|
3763
|
+
throw new Error("NOT_AVAILABLE:skill installer is not configured");
|
|
3764
|
+
}
|
|
3765
|
+
const result = await installer.installSkill({
|
|
3766
|
+
slug: spec,
|
|
3767
|
+
kind: params.body.kind,
|
|
3768
|
+
skill: params.body.skill,
|
|
3769
|
+
installPath: params.body.installPath,
|
|
3770
|
+
force: params.body.force
|
|
3387
3771
|
});
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3772
|
+
params.options.publish({ type: "config.updated", payload: { path: "skills" } });
|
|
3773
|
+
return {
|
|
3774
|
+
type: "skill",
|
|
3775
|
+
spec,
|
|
3776
|
+
message: result.message,
|
|
3777
|
+
output: result.output
|
|
3778
|
+
};
|
|
3779
|
+
}
|
|
3780
|
+
async function manageMarketplaceSkill(params) {
|
|
3781
|
+
const action = params.body.action;
|
|
3782
|
+
const targetId = typeof params.body.id === "string" && params.body.id.trim().length > 0 ? params.body.id.trim() : typeof params.body.spec === "string" && params.body.spec.trim().length > 0 ? params.body.spec.trim() : "";
|
|
3783
|
+
if (action !== "uninstall" || !targetId) {
|
|
3784
|
+
throw new Error("INVALID_BODY:skill manage requires uninstall action and non-empty id/spec");
|
|
3785
|
+
}
|
|
3786
|
+
const installer = params.options.marketplace?.installer;
|
|
3787
|
+
if (!installer) {
|
|
3788
|
+
throw new Error("NOT_AVAILABLE:marketplace installer is not configured");
|
|
3789
|
+
}
|
|
3790
|
+
if (!installer.uninstallSkill) {
|
|
3791
|
+
throw new Error("NOT_AVAILABLE:skill uninstall is not configured");
|
|
3792
|
+
}
|
|
3793
|
+
const result = await installer.uninstallSkill(targetId);
|
|
3794
|
+
params.options.publish({ type: "config.updated", payload: { path: "skills" } });
|
|
3795
|
+
return {
|
|
3796
|
+
type: "skill",
|
|
3797
|
+
action,
|
|
3798
|
+
id: targetId,
|
|
3799
|
+
message: result.message,
|
|
3800
|
+
output: result.output
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
var SkillMarketplaceController = class {
|
|
3804
|
+
constructor(options, marketplaceBaseUrl) {
|
|
3805
|
+
this.options = options;
|
|
3806
|
+
this.marketplaceBaseUrl = marketplaceBaseUrl;
|
|
3807
|
+
}
|
|
3808
|
+
getInstalled = (c) => {
|
|
3809
|
+
return c.json(ok(collectSkillMarketplaceInstalledView(this.options)));
|
|
3810
|
+
};
|
|
3811
|
+
listItems = async (c) => {
|
|
3812
|
+
const query = c.req.query();
|
|
3813
|
+
const result = await fetchAllSkillMarketplaceItems({
|
|
3814
|
+
baseUrl: this.marketplaceBaseUrl,
|
|
3815
|
+
query: {
|
|
3816
|
+
q: query.q,
|
|
3817
|
+
tag: query.tag,
|
|
3818
|
+
sort: query.sort,
|
|
3819
|
+
page: query.page,
|
|
3820
|
+
pageSize: query.pageSize
|
|
3821
|
+
}
|
|
3822
|
+
});
|
|
3823
|
+
if (!result.ok) {
|
|
3824
|
+
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3395
3825
|
}
|
|
3396
|
-
const
|
|
3397
|
-
|
|
3398
|
-
|
|
3826
|
+
const normalizedItems = sanitizeMarketplaceListItems(result.data.items).map((item) => normalizeMarketplaceItemForUi(item));
|
|
3827
|
+
const unsupportedKind = findUnsupportedSkillInstallKind(normalizedItems);
|
|
3828
|
+
if (unsupportedKind) {
|
|
3829
|
+
return c.json(
|
|
3830
|
+
err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
|
|
3831
|
+
502
|
|
3832
|
+
);
|
|
3399
3833
|
}
|
|
3400
|
-
const
|
|
3401
|
-
const
|
|
3402
|
-
const
|
|
3403
|
-
const
|
|
3404
|
-
const
|
|
3405
|
-
const
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
};
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
});
|
|
3425
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3426
|
-
return c.json(ok(response));
|
|
3427
|
-
} catch (error) {
|
|
3428
|
-
return c.json(err("CHAT_TURN_FAILED", formatUserFacingError(error)), 500);
|
|
3834
|
+
const knownSkillNames = collectKnownSkillNames(this.options);
|
|
3835
|
+
const filteredItems = normalizedItems.filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
|
|
3836
|
+
const pageSize = Math.min(100, toPositiveInt(query.pageSize, 20));
|
|
3837
|
+
const requestedPage = toPositiveInt(query.page, 1);
|
|
3838
|
+
const totalPages = filteredItems.length === 0 ? 0 : Math.ceil(filteredItems.length / pageSize);
|
|
3839
|
+
const currentPage = totalPages === 0 ? 1 : Math.min(requestedPage, totalPages);
|
|
3840
|
+
return c.json(ok({
|
|
3841
|
+
total: filteredItems.length,
|
|
3842
|
+
page: currentPage,
|
|
3843
|
+
pageSize,
|
|
3844
|
+
totalPages,
|
|
3845
|
+
sort: result.data.sort,
|
|
3846
|
+
query: result.data.query,
|
|
3847
|
+
items: filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
|
3848
|
+
}));
|
|
3849
|
+
};
|
|
3850
|
+
getItem = async (c) => {
|
|
3851
|
+
const slug = encodeURIComponent(c.req.param("slug"));
|
|
3852
|
+
const result = await fetchMarketplaceData({
|
|
3853
|
+
baseUrl: this.marketplaceBaseUrl,
|
|
3854
|
+
path: `/api/v1/skills/items/${slug}`
|
|
3855
|
+
});
|
|
3856
|
+
if (!result.ok) {
|
|
3857
|
+
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3429
3858
|
}
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
const
|
|
3433
|
-
if (
|
|
3434
|
-
return c.json(
|
|
3859
|
+
const knownSkillNames = collectKnownSkillNames(this.options);
|
|
3860
|
+
const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItemView(result.data));
|
|
3861
|
+
const unsupportedKind = findUnsupportedSkillInstallKind([sanitized]);
|
|
3862
|
+
if (unsupportedKind) {
|
|
3863
|
+
return c.json(
|
|
3864
|
+
err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
|
|
3865
|
+
502
|
|
3866
|
+
);
|
|
3435
3867
|
}
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3868
|
+
if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) {
|
|
3869
|
+
return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
|
|
3439
3870
|
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3871
|
+
return c.json(ok(sanitized));
|
|
3872
|
+
};
|
|
3873
|
+
getItemContent = async (c) => {
|
|
3874
|
+
const slug = encodeURIComponent(c.req.param("slug"));
|
|
3875
|
+
const result = await fetchMarketplaceData({
|
|
3876
|
+
baseUrl: this.marketplaceBaseUrl,
|
|
3877
|
+
path: `/api/v1/skills/items/${slug}`
|
|
3878
|
+
});
|
|
3879
|
+
if (!result.ok) {
|
|
3880
|
+
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3443
3881
|
}
|
|
3444
|
-
const
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
} catch (error) {
|
|
3453
|
-
return c.json(err("CHAT_TURN_STOP_FAILED", String(error)), 500);
|
|
3882
|
+
const knownSkillNames = collectKnownSkillNames(this.options);
|
|
3883
|
+
const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItemView(result.data));
|
|
3884
|
+
const unsupportedKind = findUnsupportedSkillInstallKind([sanitized]);
|
|
3885
|
+
if (unsupportedKind) {
|
|
3886
|
+
return c.json(
|
|
3887
|
+
err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
|
|
3888
|
+
502
|
|
3889
|
+
);
|
|
3454
3890
|
}
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3891
|
+
if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) {
|
|
3892
|
+
return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
|
|
3893
|
+
}
|
|
3894
|
+
const contentResult = await fetchMarketplaceData({
|
|
3895
|
+
baseUrl: this.marketplaceBaseUrl,
|
|
3896
|
+
path: `/api/v1/skills/items/${slug}/content`
|
|
3897
|
+
});
|
|
3898
|
+
if (!contentResult.ok) {
|
|
3899
|
+
return c.json(err("MARKETPLACE_UNAVAILABLE", contentResult.message), contentResult.status);
|
|
3460
3900
|
}
|
|
3901
|
+
return c.json(ok(contentResult.data));
|
|
3902
|
+
};
|
|
3903
|
+
install = async (c) => {
|
|
3461
3904
|
const body = await readJson(c.req.raw);
|
|
3462
|
-
if (!body.ok) {
|
|
3905
|
+
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
3463
3906
|
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3464
3907
|
}
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
return c.json(err("INVALID_BODY", "message is required"), 400);
|
|
3468
|
-
}
|
|
3469
|
-
const sessionKey = readNonEmptyString(body.data.sessionKey) ?? `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
|
|
3470
|
-
const requestedAt = /* @__PURE__ */ new Date();
|
|
3471
|
-
const startedAtMs = requestedAt.getTime();
|
|
3472
|
-
const metadata = isRecord(body.data.metadata) ? body.data.metadata : void 0;
|
|
3473
|
-
const requestedAgentId = readNonEmptyString(body.data.agentId) ?? resolveAgentIdFromSessionKey(sessionKey);
|
|
3474
|
-
const requestedModel = readNonEmptyString(body.data.model);
|
|
3475
|
-
let runId = createChatRunId();
|
|
3476
|
-
const supportsManagedRuns = Boolean(chatRuntime.startTurnRun && chatRuntime.streamRun);
|
|
3477
|
-
let stopCapabilities = { stopSupported: Boolean(chatRuntime.stopTurn) };
|
|
3478
|
-
if (chatRuntime.getCapabilities) {
|
|
3479
|
-
try {
|
|
3480
|
-
stopCapabilities = await chatRuntime.getCapabilities({
|
|
3481
|
-
sessionKey,
|
|
3482
|
-
...requestedAgentId ? { agentId: requestedAgentId } : {}
|
|
3483
|
-
});
|
|
3484
|
-
} catch {
|
|
3485
|
-
stopCapabilities = {
|
|
3486
|
-
stopSupported: false,
|
|
3487
|
-
stopReason: "failed to resolve runtime stop capability"
|
|
3488
|
-
};
|
|
3489
|
-
}
|
|
3490
|
-
}
|
|
3491
|
-
const request = {
|
|
3492
|
-
message,
|
|
3493
|
-
sessionKey,
|
|
3494
|
-
channel: readNonEmptyString(body.data.channel) ?? "ui",
|
|
3495
|
-
chatId: readNonEmptyString(body.data.chatId) ?? "web-ui",
|
|
3496
|
-
runId,
|
|
3497
|
-
...requestedAgentId ? { agentId: requestedAgentId } : {},
|
|
3498
|
-
...requestedModel ? { model: requestedModel } : {},
|
|
3499
|
-
...metadata ? { metadata } : {}
|
|
3500
|
-
};
|
|
3501
|
-
let managedRun = null;
|
|
3502
|
-
if (supportsManagedRuns && chatRuntime.startTurnRun) {
|
|
3503
|
-
try {
|
|
3504
|
-
managedRun = await chatRuntime.startTurnRun(request);
|
|
3505
|
-
} catch (error) {
|
|
3506
|
-
return c.json(err("CHAT_TURN_FAILED", formatUserFacingError(error)), 500);
|
|
3507
|
-
}
|
|
3508
|
-
if (readNonEmptyString(managedRun.runId)) {
|
|
3509
|
-
runId = readNonEmptyString(managedRun.runId);
|
|
3510
|
-
}
|
|
3511
|
-
stopCapabilities = {
|
|
3512
|
-
stopSupported: managedRun.stopSupported,
|
|
3513
|
-
...readNonEmptyString(managedRun.stopReason) ? { stopReason: readNonEmptyString(managedRun.stopReason) } : {}
|
|
3514
|
-
};
|
|
3515
|
-
}
|
|
3516
|
-
const encoder = new TextEncoder();
|
|
3517
|
-
const stream = new ReadableStream({
|
|
3518
|
-
start: async (controller) => {
|
|
3519
|
-
const push = (event, data) => {
|
|
3520
|
-
controller.enqueue(encoder.encode(toSseFrame(event, data)));
|
|
3521
|
-
};
|
|
3522
|
-
try {
|
|
3523
|
-
push("ready", {
|
|
3524
|
-
sessionKey: managedRun?.sessionKey ?? sessionKey,
|
|
3525
|
-
requestedAt: managedRun?.requestedAt ?? requestedAt.toISOString(),
|
|
3526
|
-
runId,
|
|
3527
|
-
stopSupported: stopCapabilities.stopSupported,
|
|
3528
|
-
...readNonEmptyString(stopCapabilities.stopReason) ? { stopReason: readNonEmptyString(stopCapabilities.stopReason) } : {}
|
|
3529
|
-
});
|
|
3530
|
-
if (supportsManagedRuns && chatRuntime.streamRun) {
|
|
3531
|
-
let hasFinal2 = false;
|
|
3532
|
-
for await (const event of chatRuntime.streamRun({ runId })) {
|
|
3533
|
-
const typed = event;
|
|
3534
|
-
if (typed.type === "delta") {
|
|
3535
|
-
if (typed.delta) {
|
|
3536
|
-
push("delta", { delta: typed.delta });
|
|
3537
|
-
}
|
|
3538
|
-
continue;
|
|
3539
|
-
}
|
|
3540
|
-
if (typed.type === "session_event") {
|
|
3541
|
-
push("session_event", typed.event);
|
|
3542
|
-
continue;
|
|
3543
|
-
}
|
|
3544
|
-
if (typed.type === "final") {
|
|
3545
|
-
const latestRun = chatRuntime.getRun ? await chatRuntime.getRun({ runId }) : null;
|
|
3546
|
-
const response = latestRun ? buildChatTurnViewFromRun({
|
|
3547
|
-
run: latestRun,
|
|
3548
|
-
fallbackSessionKey: sessionKey,
|
|
3549
|
-
fallbackAgentId: requestedAgentId,
|
|
3550
|
-
fallbackModel: requestedModel,
|
|
3551
|
-
fallbackReply: typed.result.reply
|
|
3552
|
-
}) : buildChatTurnView({
|
|
3553
|
-
result: typed.result,
|
|
3554
|
-
fallbackSessionKey: sessionKey,
|
|
3555
|
-
requestedAgentId,
|
|
3556
|
-
requestedModel,
|
|
3557
|
-
requestedAt,
|
|
3558
|
-
startedAtMs
|
|
3559
|
-
});
|
|
3560
|
-
hasFinal2 = true;
|
|
3561
|
-
push("final", response);
|
|
3562
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3563
|
-
continue;
|
|
3564
|
-
}
|
|
3565
|
-
if (typed.type === "error") {
|
|
3566
|
-
push("error", {
|
|
3567
|
-
code: "CHAT_TURN_FAILED",
|
|
3568
|
-
message: formatUserFacingError(typed.error)
|
|
3569
|
-
});
|
|
3570
|
-
return;
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
if (!hasFinal2) {
|
|
3574
|
-
push("error", {
|
|
3575
|
-
code: "CHAT_TURN_FAILED",
|
|
3576
|
-
message: "stream ended without a final result"
|
|
3577
|
-
});
|
|
3578
|
-
return;
|
|
3579
|
-
}
|
|
3580
|
-
push("done", { ok: true });
|
|
3581
|
-
return;
|
|
3582
|
-
}
|
|
3583
|
-
const streamTurn = chatRuntime.processTurnStream;
|
|
3584
|
-
if (!streamTurn) {
|
|
3585
|
-
const result = await chatRuntime.processTurn(request);
|
|
3586
|
-
const response = buildChatTurnView({
|
|
3587
|
-
result,
|
|
3588
|
-
fallbackSessionKey: sessionKey,
|
|
3589
|
-
requestedAgentId,
|
|
3590
|
-
requestedModel,
|
|
3591
|
-
requestedAt,
|
|
3592
|
-
startedAtMs
|
|
3593
|
-
});
|
|
3594
|
-
push("final", response);
|
|
3595
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3596
|
-
push("done", { ok: true });
|
|
3597
|
-
return;
|
|
3598
|
-
}
|
|
3599
|
-
let hasFinal = false;
|
|
3600
|
-
for await (const event of streamTurn(request)) {
|
|
3601
|
-
const typed = event;
|
|
3602
|
-
if (typed.type === "delta") {
|
|
3603
|
-
if (typed.delta) {
|
|
3604
|
-
push("delta", { delta: typed.delta });
|
|
3605
|
-
}
|
|
3606
|
-
continue;
|
|
3607
|
-
}
|
|
3608
|
-
if (typed.type === "session_event") {
|
|
3609
|
-
push("session_event", typed.event);
|
|
3610
|
-
continue;
|
|
3611
|
-
}
|
|
3612
|
-
if (typed.type === "final") {
|
|
3613
|
-
const response = buildChatTurnView({
|
|
3614
|
-
result: typed.result,
|
|
3615
|
-
fallbackSessionKey: sessionKey,
|
|
3616
|
-
requestedAgentId,
|
|
3617
|
-
requestedModel,
|
|
3618
|
-
requestedAt,
|
|
3619
|
-
startedAtMs
|
|
3620
|
-
});
|
|
3621
|
-
hasFinal = true;
|
|
3622
|
-
push("final", response);
|
|
3623
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3624
|
-
continue;
|
|
3625
|
-
}
|
|
3626
|
-
if (typed.type === "error") {
|
|
3627
|
-
push("error", {
|
|
3628
|
-
code: "CHAT_TURN_FAILED",
|
|
3629
|
-
message: formatUserFacingError(typed.error)
|
|
3630
|
-
});
|
|
3631
|
-
return;
|
|
3632
|
-
}
|
|
3633
|
-
}
|
|
3634
|
-
if (!hasFinal) {
|
|
3635
|
-
push("error", {
|
|
3636
|
-
code: "CHAT_TURN_FAILED",
|
|
3637
|
-
message: "stream ended without a final result"
|
|
3638
|
-
});
|
|
3639
|
-
return;
|
|
3640
|
-
}
|
|
3641
|
-
push("done", { ok: true });
|
|
3642
|
-
} catch (error) {
|
|
3643
|
-
push("error", {
|
|
3644
|
-
code: "CHAT_TURN_FAILED",
|
|
3645
|
-
message: formatUserFacingError(error)
|
|
3646
|
-
});
|
|
3647
|
-
} finally {
|
|
3648
|
-
controller.close();
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
});
|
|
3652
|
-
return new Response(stream, {
|
|
3653
|
-
status: 200,
|
|
3654
|
-
headers: {
|
|
3655
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
3656
|
-
"Cache-Control": "no-cache, no-transform",
|
|
3657
|
-
"Connection": "keep-alive",
|
|
3658
|
-
"X-Accel-Buffering": "no"
|
|
3659
|
-
}
|
|
3660
|
-
});
|
|
3661
|
-
});
|
|
3662
|
-
app.get("/api/chat/runs", async (c) => {
|
|
3663
|
-
const chatRuntime = options.chatRuntime;
|
|
3664
|
-
if (!chatRuntime?.listRuns) {
|
|
3665
|
-
return c.json(err("NOT_AVAILABLE", "chat run management unavailable"), 503);
|
|
3908
|
+
if (body.data.type && body.data.type !== "skill") {
|
|
3909
|
+
return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
|
|
3666
3910
|
}
|
|
3667
|
-
const query = c.req.query();
|
|
3668
|
-
const sessionKey = readNonEmptyString(query.sessionKey);
|
|
3669
|
-
const states = readChatRunStates(query.states);
|
|
3670
|
-
const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
|
|
3671
3911
|
try {
|
|
3672
|
-
const
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
...Number.isFinite(limit) ? { limit } : {}
|
|
3912
|
+
const payload = await installMarketplaceSkill({
|
|
3913
|
+
options: this.options,
|
|
3914
|
+
body: body.data
|
|
3676
3915
|
});
|
|
3677
|
-
return c.json(ok(
|
|
3916
|
+
return c.json(ok(payload));
|
|
3678
3917
|
} catch (error) {
|
|
3679
|
-
|
|
3918
|
+
const message = String(error);
|
|
3919
|
+
if (message.startsWith("INVALID_BODY:")) {
|
|
3920
|
+
return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
|
|
3921
|
+
}
|
|
3922
|
+
if (message.startsWith("NOT_AVAILABLE:")) {
|
|
3923
|
+
return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
|
|
3924
|
+
}
|
|
3925
|
+
return c.json(err("INSTALL_FAILED", message), 400);
|
|
3680
3926
|
}
|
|
3681
|
-
}
|
|
3682
|
-
|
|
3683
|
-
const
|
|
3684
|
-
if (!
|
|
3685
|
-
return c.json(err("
|
|
3927
|
+
};
|
|
3928
|
+
manage = async (c) => {
|
|
3929
|
+
const body = await readJson(c.req.raw);
|
|
3930
|
+
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
3931
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3686
3932
|
}
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
return c.json(err("INVALID_PATH", "runId is required"), 400);
|
|
3933
|
+
if (body.data.type && body.data.type !== "skill") {
|
|
3934
|
+
return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
|
|
3690
3935
|
}
|
|
3691
3936
|
try {
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
}
|
|
3696
|
-
return c.json(ok(
|
|
3937
|
+
const payload = await manageMarketplaceSkill({
|
|
3938
|
+
options: this.options,
|
|
3939
|
+
body: body.data
|
|
3940
|
+
});
|
|
3941
|
+
return c.json(ok(payload));
|
|
3697
3942
|
} catch (error) {
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
return c.json(err("NOT_AVAILABLE", "chat run stream unavailable"), 503);
|
|
3707
|
-
}
|
|
3708
|
-
const runId = readNonEmptyString(c.req.param("runId"));
|
|
3709
|
-
if (!runId) {
|
|
3710
|
-
return c.json(err("INVALID_PATH", "runId is required"), 400);
|
|
3943
|
+
const message = String(error);
|
|
3944
|
+
if (message.startsWith("INVALID_BODY:")) {
|
|
3945
|
+
return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
|
|
3946
|
+
}
|
|
3947
|
+
if (message.startsWith("NOT_AVAILABLE:")) {
|
|
3948
|
+
return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
|
|
3949
|
+
}
|
|
3950
|
+
return c.json(err("MANAGE_FAILED", message), 400);
|
|
3711
3951
|
}
|
|
3952
|
+
};
|
|
3953
|
+
getRecommendations = async (c) => {
|
|
3712
3954
|
const query = c.req.query();
|
|
3713
|
-
const
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
const stream = new ReadableStream({
|
|
3720
|
-
start: async (controller) => {
|
|
3721
|
-
const push = (event, data) => {
|
|
3722
|
-
controller.enqueue(encoder.encode(toSseFrame(event, data)));
|
|
3723
|
-
};
|
|
3724
|
-
try {
|
|
3725
|
-
push("ready", {
|
|
3726
|
-
sessionKey: run.sessionKey,
|
|
3727
|
-
requestedAt: run.requestedAt,
|
|
3728
|
-
runId: run.runId,
|
|
3729
|
-
stopSupported: run.stopSupported,
|
|
3730
|
-
...readNonEmptyString(run.stopReason) ? { stopReason: readNonEmptyString(run.stopReason) } : {}
|
|
3731
|
-
});
|
|
3732
|
-
let hasFinal = false;
|
|
3733
|
-
for await (const event of streamRun({
|
|
3734
|
-
runId: run.runId,
|
|
3735
|
-
...Number.isFinite(fromEventIndex) ? { fromEventIndex } : {}
|
|
3736
|
-
})) {
|
|
3737
|
-
const typed = event;
|
|
3738
|
-
if (typed.type === "delta") {
|
|
3739
|
-
if (typed.delta) {
|
|
3740
|
-
push("delta", { delta: typed.delta });
|
|
3741
|
-
}
|
|
3742
|
-
continue;
|
|
3743
|
-
}
|
|
3744
|
-
if (typed.type === "session_event") {
|
|
3745
|
-
push("session_event", typed.event);
|
|
3746
|
-
continue;
|
|
3747
|
-
}
|
|
3748
|
-
if (typed.type === "final") {
|
|
3749
|
-
const latestRun = await getRun({ runId: run.runId });
|
|
3750
|
-
const response = latestRun ? buildChatTurnViewFromRun({
|
|
3751
|
-
run: latestRun,
|
|
3752
|
-
fallbackSessionKey: run.sessionKey,
|
|
3753
|
-
fallbackAgentId: run.agentId,
|
|
3754
|
-
fallbackModel: run.model,
|
|
3755
|
-
fallbackReply: typed.result.reply
|
|
3756
|
-
}) : buildChatTurnView({
|
|
3757
|
-
result: typed.result,
|
|
3758
|
-
fallbackSessionKey: run.sessionKey,
|
|
3759
|
-
requestedAgentId: run.agentId,
|
|
3760
|
-
requestedModel: run.model,
|
|
3761
|
-
requestedAt: new Date(run.requestedAt),
|
|
3762
|
-
startedAtMs: Date.parse(run.requestedAt)
|
|
3763
|
-
});
|
|
3764
|
-
hasFinal = true;
|
|
3765
|
-
push("final", response);
|
|
3766
|
-
continue;
|
|
3767
|
-
}
|
|
3768
|
-
if (typed.type === "error") {
|
|
3769
|
-
push("error", {
|
|
3770
|
-
code: "CHAT_TURN_FAILED",
|
|
3771
|
-
message: formatUserFacingError(typed.error)
|
|
3772
|
-
});
|
|
3773
|
-
return;
|
|
3774
|
-
}
|
|
3775
|
-
}
|
|
3776
|
-
if (!hasFinal) {
|
|
3777
|
-
const latestRun = await getRun({ runId: run.runId });
|
|
3778
|
-
if (latestRun?.state === "failed") {
|
|
3779
|
-
push("error", {
|
|
3780
|
-
code: "CHAT_TURN_FAILED",
|
|
3781
|
-
message: formatUserFacingError(latestRun.error ?? "chat run failed")
|
|
3782
|
-
});
|
|
3783
|
-
return;
|
|
3784
|
-
}
|
|
3785
|
-
}
|
|
3786
|
-
push("done", { ok: true });
|
|
3787
|
-
} catch (error) {
|
|
3788
|
-
push("error", {
|
|
3789
|
-
code: "CHAT_TURN_FAILED",
|
|
3790
|
-
message: formatUserFacingError(error)
|
|
3791
|
-
});
|
|
3792
|
-
} finally {
|
|
3793
|
-
controller.close();
|
|
3794
|
-
}
|
|
3795
|
-
}
|
|
3796
|
-
});
|
|
3797
|
-
return new Response(stream, {
|
|
3798
|
-
status: 200,
|
|
3799
|
-
headers: {
|
|
3800
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
3801
|
-
"Cache-Control": "no-cache, no-transform",
|
|
3802
|
-
"Connection": "keep-alive",
|
|
3803
|
-
"X-Accel-Buffering": "no"
|
|
3955
|
+
const result = await fetchMarketplaceData({
|
|
3956
|
+
baseUrl: this.marketplaceBaseUrl,
|
|
3957
|
+
path: "/api/v1/skills/recommendations",
|
|
3958
|
+
query: {
|
|
3959
|
+
scene: query.scene,
|
|
3960
|
+
limit: query.limit
|
|
3804
3961
|
}
|
|
3805
3962
|
});
|
|
3806
|
-
|
|
3807
|
-
|
|
3963
|
+
if (!result.ok) {
|
|
3964
|
+
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3965
|
+
}
|
|
3966
|
+
const knownSkillNames = collectKnownSkillNames(this.options);
|
|
3967
|
+
const filteredItems = sanitizeMarketplaceListItems(result.data.items).map((item) => normalizeMarketplaceItemForUi(item)).filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
|
|
3968
|
+
return c.json(ok({
|
|
3969
|
+
...result.data,
|
|
3970
|
+
total: filteredItems.length,
|
|
3971
|
+
items: filteredItems
|
|
3972
|
+
}));
|
|
3973
|
+
};
|
|
3974
|
+
};
|
|
3975
|
+
|
|
3976
|
+
// src/ui/router/session.controller.ts
|
|
3977
|
+
var SessionRoutesController = class {
|
|
3978
|
+
constructor(options) {
|
|
3979
|
+
this.options = options;
|
|
3980
|
+
}
|
|
3981
|
+
listSessions = (c) => {
|
|
3808
3982
|
const query = c.req.query();
|
|
3809
3983
|
const q = typeof query.q === "string" ? query.q : void 0;
|
|
3810
3984
|
const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
|
|
3811
3985
|
const activeMinutes = typeof query.activeMinutes === "string" ? Number.parseInt(query.activeMinutes, 10) : void 0;
|
|
3812
|
-
const data = listSessions(options.configPath, {
|
|
3986
|
+
const data = listSessions(this.options.configPath, {
|
|
3813
3987
|
q,
|
|
3814
3988
|
limit: Number.isFinite(limit) ? limit : void 0,
|
|
3815
3989
|
activeMinutes: Number.isFinite(activeMinutes) ? activeMinutes : void 0
|
|
3816
3990
|
});
|
|
3817
3991
|
return c.json(ok(data));
|
|
3818
|
-
}
|
|
3819
|
-
|
|
3992
|
+
};
|
|
3993
|
+
getSessionHistory = (c) => {
|
|
3820
3994
|
const key = decodeURIComponent(c.req.param("key"));
|
|
3821
3995
|
const query = c.req.query();
|
|
3822
3996
|
const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
|
|
3823
|
-
const data = getSessionHistory(options.configPath, key, Number.isFinite(limit) ? limit : void 0);
|
|
3997
|
+
const data = getSessionHistory(this.options.configPath, key, Number.isFinite(limit) ? limit : void 0);
|
|
3824
3998
|
if (!data) {
|
|
3825
3999
|
return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
|
|
3826
4000
|
}
|
|
3827
4001
|
return c.json(ok(data));
|
|
3828
|
-
}
|
|
3829
|
-
|
|
4002
|
+
};
|
|
4003
|
+
patchSession = async (c) => {
|
|
3830
4004
|
const key = decodeURIComponent(c.req.param("key"));
|
|
3831
4005
|
const body = await readJson(c.req.raw);
|
|
3832
4006
|
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
@@ -3834,12 +4008,12 @@ function createUiRouter(options) {
|
|
|
3834
4008
|
}
|
|
3835
4009
|
let availableSessionTypes;
|
|
3836
4010
|
if (Object.prototype.hasOwnProperty.call(body.data, "sessionType")) {
|
|
3837
|
-
const sessionTypes = await buildChatSessionTypesView(options.chatRuntime);
|
|
4011
|
+
const sessionTypes = await buildChatSessionTypesView(this.options.chatRuntime);
|
|
3838
4012
|
availableSessionTypes = sessionTypes.options.map((item) => item.value);
|
|
3839
4013
|
}
|
|
3840
4014
|
let data;
|
|
3841
4015
|
try {
|
|
3842
|
-
data = patchSession(options.configPath, key, body.data, {
|
|
4016
|
+
data = patchSession(this.options.configPath, key, body.data, {
|
|
3843
4017
|
...availableSessionTypes ? { availableSessionTypes } : {}
|
|
3844
4018
|
});
|
|
3845
4019
|
} catch (error) {
|
|
@@ -3854,111 +4028,81 @@ function createUiRouter(options) {
|
|
|
3854
4028
|
if (!data) {
|
|
3855
4029
|
return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
|
|
3856
4030
|
}
|
|
3857
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
4031
|
+
this.options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3858
4032
|
return c.json(ok(data));
|
|
3859
|
-
}
|
|
3860
|
-
|
|
4033
|
+
};
|
|
4034
|
+
deleteSession = (c) => {
|
|
3861
4035
|
const key = decodeURIComponent(c.req.param("key"));
|
|
3862
|
-
const deleted = deleteSession(options.configPath, key);
|
|
4036
|
+
const deleted = deleteSession(this.options.configPath, key);
|
|
3863
4037
|
if (!deleted) {
|
|
3864
4038
|
return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
|
|
3865
4039
|
}
|
|
3866
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3867
|
-
return c.json(ok({ deleted: true }));
|
|
3868
|
-
});
|
|
3869
|
-
app.get("/api/cron", (c) => {
|
|
3870
|
-
if (!options.cronService) {
|
|
3871
|
-
return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
|
|
3872
|
-
}
|
|
3873
|
-
const query = c.req.query();
|
|
3874
|
-
const includeDisabled = query.all === "1" || query.all === "true" || query.all === "yes";
|
|
3875
|
-
const jobs = options.cronService.listJobs(includeDisabled).map((job) => buildCronJobView(job));
|
|
3876
|
-
return c.json(ok({ jobs, total: jobs.length }));
|
|
3877
|
-
});
|
|
3878
|
-
app.delete("/api/cron/:id", (c) => {
|
|
3879
|
-
if (!options.cronService) {
|
|
3880
|
-
return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
|
|
3881
|
-
}
|
|
3882
|
-
const id = decodeURIComponent(c.req.param("id"));
|
|
3883
|
-
const deleted = options.cronService.removeJob(id);
|
|
3884
|
-
if (!deleted) {
|
|
3885
|
-
return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
|
|
3886
|
-
}
|
|
4040
|
+
this.options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3887
4041
|
return c.json(ok({ deleted: true }));
|
|
3888
|
-
}
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
app.
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
app.
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
app.post("/api/
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
if (!body.ok) {
|
|
3953
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3954
|
-
}
|
|
3955
|
-
const result = await executeConfigAction(options.configPath, actionId, body.data ?? {});
|
|
3956
|
-
if (!result.ok) {
|
|
3957
|
-
return c.json(err(result.code, result.message, result.details), 400);
|
|
3958
|
-
}
|
|
3959
|
-
return c.json(ok(result.data));
|
|
3960
|
-
});
|
|
3961
|
-
registerMarketplaceRoutes(app, options, marketplaceBaseUrl);
|
|
4042
|
+
};
|
|
4043
|
+
};
|
|
4044
|
+
|
|
4045
|
+
// src/ui/router.ts
|
|
4046
|
+
function createUiRouter(options) {
|
|
4047
|
+
const app = new Hono();
|
|
4048
|
+
const marketplaceBaseUrl = normalizeMarketplaceBaseUrl(options);
|
|
4049
|
+
const appController = new AppRoutesController(options);
|
|
4050
|
+
const configController = new ConfigRoutesController(options);
|
|
4051
|
+
const chatController = new ChatRoutesController(options);
|
|
4052
|
+
const sessionController = new SessionRoutesController(options);
|
|
4053
|
+
const cronController = new CronRoutesController(options);
|
|
4054
|
+
const pluginMarketplaceController = new PluginMarketplaceController(options, marketplaceBaseUrl);
|
|
4055
|
+
const skillMarketplaceController = new SkillMarketplaceController(options, marketplaceBaseUrl);
|
|
4056
|
+
app.notFound((c) => c.json(err("NOT_FOUND", "endpoint not found"), 404));
|
|
4057
|
+
app.get("/api/health", appController.health);
|
|
4058
|
+
app.get("/api/app/meta", appController.appMeta);
|
|
4059
|
+
app.get("/api/config", configController.getConfig);
|
|
4060
|
+
app.get("/api/config/meta", configController.getConfigMeta);
|
|
4061
|
+
app.get("/api/config/schema", configController.getConfigSchema);
|
|
4062
|
+
app.put("/api/config/model", configController.updateConfigModel);
|
|
4063
|
+
app.put("/api/config/search", configController.updateConfigSearch);
|
|
4064
|
+
app.put("/api/config/providers/:provider", configController.updateProvider);
|
|
4065
|
+
app.post("/api/config/providers", configController.createProvider);
|
|
4066
|
+
app.delete("/api/config/providers/:provider", configController.deleteProvider);
|
|
4067
|
+
app.post("/api/config/providers/:provider/test", configController.testProviderConnection);
|
|
4068
|
+
app.post("/api/config/providers/:provider/auth/start", configController.startProviderAuth);
|
|
4069
|
+
app.post("/api/config/providers/:provider/auth/poll", configController.pollProviderAuth);
|
|
4070
|
+
app.post("/api/config/providers/:provider/auth/import-cli", configController.importProviderAuthFromCli);
|
|
4071
|
+
app.put("/api/config/channels/:channel", configController.updateChannel);
|
|
4072
|
+
app.put("/api/config/secrets", configController.updateSecrets);
|
|
4073
|
+
app.put("/api/config/runtime", configController.updateRuntime);
|
|
4074
|
+
app.post("/api/config/actions/:actionId/execute", configController.executeAction);
|
|
4075
|
+
app.get("/api/chat/capabilities", chatController.getCapabilities);
|
|
4076
|
+
app.get("/api/chat/session-types", chatController.getSessionTypes);
|
|
4077
|
+
app.get("/api/chat/commands", chatController.getCommands);
|
|
4078
|
+
app.post("/api/chat/turn", chatController.processTurn);
|
|
4079
|
+
app.post("/api/chat/turn/stop", chatController.stopTurn);
|
|
4080
|
+
app.post("/api/chat/turn/stream", chatController.streamTurn);
|
|
4081
|
+
app.get("/api/chat/runs", chatController.listRuns);
|
|
4082
|
+
app.get("/api/chat/runs/:runId", chatController.getRun);
|
|
4083
|
+
app.get("/api/chat/runs/:runId/stream", chatController.streamRun);
|
|
4084
|
+
app.get("/api/sessions", sessionController.listSessions);
|
|
4085
|
+
app.get("/api/sessions/:key/history", sessionController.getSessionHistory);
|
|
4086
|
+
app.put("/api/sessions/:key", sessionController.patchSession);
|
|
4087
|
+
app.delete("/api/sessions/:key", sessionController.deleteSession);
|
|
4088
|
+
app.get("/api/cron", cronController.listJobs);
|
|
4089
|
+
app.delete("/api/cron/:id", cronController.deleteJob);
|
|
4090
|
+
app.put("/api/cron/:id/enable", cronController.enableJob);
|
|
4091
|
+
app.post("/api/cron/:id/run", cronController.runJob);
|
|
4092
|
+
app.get("/api/marketplace/plugins/installed", pluginMarketplaceController.getInstalled);
|
|
4093
|
+
app.get("/api/marketplace/plugins/items", pluginMarketplaceController.listItems);
|
|
4094
|
+
app.get("/api/marketplace/plugins/items/:slug", pluginMarketplaceController.getItem);
|
|
4095
|
+
app.get("/api/marketplace/plugins/items/:slug/content", pluginMarketplaceController.getItemContent);
|
|
4096
|
+
app.post("/api/marketplace/plugins/install", pluginMarketplaceController.install);
|
|
4097
|
+
app.post("/api/marketplace/plugins/manage", pluginMarketplaceController.manage);
|
|
4098
|
+
app.get("/api/marketplace/plugins/recommendations", pluginMarketplaceController.getRecommendations);
|
|
4099
|
+
app.get("/api/marketplace/skills/installed", skillMarketplaceController.getInstalled);
|
|
4100
|
+
app.get("/api/marketplace/skills/items", skillMarketplaceController.listItems);
|
|
4101
|
+
app.get("/api/marketplace/skills/items/:slug", skillMarketplaceController.getItem);
|
|
4102
|
+
app.get("/api/marketplace/skills/items/:slug/content", skillMarketplaceController.getItemContent);
|
|
4103
|
+
app.post("/api/marketplace/skills/install", skillMarketplaceController.install);
|
|
4104
|
+
app.post("/api/marketplace/skills/manage", skillMarketplaceController.manage);
|
|
4105
|
+
app.get("/api/marketplace/skills/recommendations", skillMarketplaceController.getRecommendations);
|
|
3962
4106
|
return app;
|
|
3963
4107
|
}
|
|
3964
4108
|
|