@nextclaw/server 0.6.11 → 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 +2246 -2091
- package/package.json +3 -7
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
|
-
|
|
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
|
+
};
|
|
2991
|
+
}
|
|
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
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
return {
|
|
3015
|
+
ok: true,
|
|
3016
|
+
data: typed.data
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
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)
|
|
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
|
|
1851
3081
|
);
|
|
1852
3082
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
if (
|
|
1857
|
-
|
|
3083
|
+
return next;
|
|
3084
|
+
}
|
|
3085
|
+
function toPositiveInt(raw, fallback) {
|
|
3086
|
+
if (!raw) {
|
|
3087
|
+
return fallback;
|
|
3088
|
+
}
|
|
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
|
+
}
|
|
1858
3123
|
}
|
|
1859
|
-
setProviderApiKey({
|
|
1860
|
-
configPath,
|
|
1861
|
-
provider: providerName,
|
|
1862
|
-
accessToken,
|
|
1863
|
-
defaultApiBase: spec.defaultApiBase
|
|
1864
|
-
});
|
|
1865
3124
|
return {
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
3125
|
+
ok: true,
|
|
3126
|
+
data: {
|
|
3127
|
+
sort,
|
|
3128
|
+
...typeof query === "string" ? { query } : {},
|
|
3129
|
+
items
|
|
3130
|
+
}
|
|
1870
3131
|
};
|
|
1871
3132
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
productVersion: productVersion && productVersion.length > 0 ? productVersion : "0.0.0"
|
|
1879
|
-
};
|
|
3133
|
+
async function fetchAllPluginMarketplaceItems(params) {
|
|
3134
|
+
return fetchAllMarketplaceItems({
|
|
3135
|
+
baseUrl: params.baseUrl,
|
|
3136
|
+
path: "/api/v1/plugins/items",
|
|
3137
|
+
query: params.query
|
|
3138
|
+
});
|
|
1880
3139
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
function
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
return new ctor(workspace);
|
|
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);
|
|
1894
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("@")) {
|
|
@@ -1952,296 +3216,56 @@ function readPluginOriginPriority(origin) {
|
|
|
1952
3216
|
return 10;
|
|
1953
3217
|
}
|
|
1954
3218
|
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
|
-
};
|
|
3219
|
+
const installScore = record.installPath ? 20 : 0;
|
|
3220
|
+
const timestampScore = record.installedAt ? 10 : 0;
|
|
3221
|
+
return readPluginRuntimeStatusPriority(record.runtimeStatus) + readPluginOriginPriority(record.origin) + installScore + timestampScore;
|
|
2168
3222
|
}
|
|
2169
|
-
function
|
|
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);
|
|
3223
|
+
function mergeInstalledPluginRecords(primary, secondary) {
|
|
2174
3224
|
return {
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
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
|
|
2182
3234
|
};
|
|
2183
3235
|
}
|
|
2184
|
-
function
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
function normalizeMarketplaceBaseUrl(options) {
|
|
2191
|
-
const fromOptions = options.marketplace?.apiBaseUrl?.trim();
|
|
2192
|
-
const fromEnv = process.env.NEXTCLAW_MARKETPLACE_API_BASE?.trim();
|
|
2193
|
-
const value = fromOptions || fromEnv || DEFAULT_MARKETPLACE_API_BASE;
|
|
2194
|
-
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
2195
|
-
}
|
|
2196
|
-
function toMarketplaceUrl(baseUrl, path, query = {}) {
|
|
2197
|
-
const url = new URL(path, `${baseUrl}/`);
|
|
2198
|
-
for (const [key, value] of Object.entries(query)) {
|
|
2199
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
2200
|
-
url.searchParams.set(key, value);
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
return url.toString();
|
|
2204
|
-
}
|
|
2205
|
-
async function fetchMarketplaceData(params) {
|
|
2206
|
-
const url = toMarketplaceUrl(params.baseUrl, params.path, params.query ?? {});
|
|
2207
|
-
try {
|
|
2208
|
-
const response = await fetch(url, {
|
|
2209
|
-
method: "GET",
|
|
2210
|
-
headers: {
|
|
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);
|
|
@@ -2385,199 +3409,49 @@ function collectPluginMarketplaceInstalledView(options) {
|
|
|
2385
3409
|
total: installed.records.length,
|
|
2386
3410
|
specs: installed.specs,
|
|
2387
3411
|
records: installed.records
|
|
2388
|
-
};
|
|
2389
|
-
}
|
|
2390
|
-
function collectSkillMarketplaceInstalledView(options) {
|
|
2391
|
-
const installed = collectInstalledSkillRecords(options);
|
|
2392
|
-
return {
|
|
2393
|
-
type: "skill",
|
|
2394
|
-
total: installed.records.length,
|
|
2395
|
-
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;
|
|
3412
|
+
};
|
|
2556
3413
|
}
|
|
2557
|
-
function
|
|
2558
|
-
const
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
3414
|
+
function collectSkillMarketplaceInstalledView(options) {
|
|
3415
|
+
const installed = collectInstalledSkillRecords(options);
|
|
3416
|
+
return {
|
|
3417
|
+
type: "skill",
|
|
3418
|
+
total: installed.records.length,
|
|
3419
|
+
specs: installed.specs,
|
|
3420
|
+
records: installed.records
|
|
2562
3421
|
};
|
|
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;
|
|
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() : "";
|
|
@@ -2791,189 +3592,34 @@ async function manageMarketplacePlugin(params) {
|
|
|
2791
3592
|
if (!installer.disablePlugin) {
|
|
2792
3593
|
throw new Error("NOT_AVAILABLE:plugin disable is not configured");
|
|
2793
3594
|
}
|
|
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);
|
|
3595
|
+
result = await installer.disablePlugin(targetId);
|
|
3596
|
+
} else {
|
|
3597
|
+
if (!installer.uninstallPlugin) {
|
|
3598
|
+
throw new Error("NOT_AVAILABLE:plugin uninstall is not configured");
|
|
2960
3599
|
}
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
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,814 +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("/api/health", (c) => c.json(ok({ status: "ok" })));
|
|
3146
|
-
app.get("/api/app/meta", (c) => c.json(ok(buildAppMetaView(options))));
|
|
3147
|
-
app.get("/api/config", (c) => {
|
|
3148
|
-
const config = loadConfigOrDefault(options.configPath);
|
|
3149
|
-
return c.json(ok(buildConfigView(config)));
|
|
3150
|
-
});
|
|
3151
|
-
app.get("/api/config/meta", (c) => {
|
|
3152
|
-
const config = loadConfigOrDefault(options.configPath);
|
|
3153
|
-
return c.json(ok(buildConfigMeta(config)));
|
|
3154
|
-
});
|
|
3155
|
-
app.get("/api/config/schema", (c) => {
|
|
3156
|
-
const config = loadConfigOrDefault(options.configPath);
|
|
3157
|
-
return c.json(ok(buildConfigSchemaView(config)));
|
|
3158
|
-
});
|
|
3159
|
-
app.put("/api/config/model", async (c) => {
|
|
3160
|
-
const body = await readJson(c.req.raw);
|
|
3161
|
-
if (!body.ok) {
|
|
3162
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3163
|
-
}
|
|
3164
|
-
const hasModel = typeof body.data.model === "string";
|
|
3165
|
-
if (!hasModel) {
|
|
3166
|
-
return c.json(err("INVALID_BODY", "model is required"), 400);
|
|
3167
|
-
}
|
|
3168
|
-
const view = updateModel(options.configPath, {
|
|
3169
|
-
model: body.data.model
|
|
3170
|
-
});
|
|
3171
|
-
if (hasModel) {
|
|
3172
|
-
options.publish({ type: "config.updated", payload: { path: "agents.defaults.model" } });
|
|
3173
|
-
}
|
|
3174
|
-
return c.json(ok({
|
|
3175
|
-
model: view.agents.defaults.model
|
|
3176
|
-
}));
|
|
3177
|
-
});
|
|
3178
|
-
app.put("/api/config/search", async (c) => {
|
|
3179
|
-
const body = await readJson(c.req.raw);
|
|
3180
|
-
if (!body.ok) {
|
|
3181
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3182
|
-
}
|
|
3183
|
-
const result = updateSearch(options.configPath, body.data);
|
|
3184
|
-
options.publish({ type: "config.updated", payload: { path: "search" } });
|
|
3185
|
-
return c.json(ok(result));
|
|
3186
|
-
});
|
|
3187
|
-
app.put("/api/config/providers/:provider", async (c) => {
|
|
3188
|
-
const provider = c.req.param("provider");
|
|
3189
|
-
const body = await readJson(c.req.raw);
|
|
3190
|
-
if (!body.ok) {
|
|
3191
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3192
|
-
}
|
|
3193
|
-
const result = updateProvider(options.configPath, provider, body.data);
|
|
3194
|
-
if (!result) {
|
|
3195
|
-
return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
|
|
3196
|
-
}
|
|
3197
|
-
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
3198
|
-
return c.json(ok(result));
|
|
3199
|
-
});
|
|
3200
|
-
app.post("/api/config/providers", async (c) => {
|
|
3201
|
-
const body = await readJson(c.req.raw);
|
|
3202
|
-
if (!body.ok) {
|
|
3203
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3204
|
-
}
|
|
3205
|
-
const result = createCustomProvider(
|
|
3206
|
-
options.configPath,
|
|
3207
|
-
body.data
|
|
3208
|
-
);
|
|
3209
|
-
options.publish({ type: "config.updated", payload: { path: `providers.${result.name}` } });
|
|
3210
|
-
return c.json(ok({
|
|
3211
|
-
name: result.name,
|
|
3212
|
-
provider: result.provider
|
|
3213
|
-
}));
|
|
3214
|
-
});
|
|
3215
|
-
app.delete("/api/config/providers/:provider", async (c) => {
|
|
3216
|
-
const provider = c.req.param("provider");
|
|
3217
|
-
const result = deleteCustomProvider(options.configPath, provider);
|
|
3218
|
-
if (result === null) {
|
|
3219
|
-
return c.json(err("NOT_FOUND", `custom provider not found: ${provider}`), 404);
|
|
3220
|
-
}
|
|
3221
|
-
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
3222
|
-
return c.json(ok({
|
|
3223
|
-
deleted: true,
|
|
3224
|
-
provider
|
|
3225
|
-
}));
|
|
3226
|
-
});
|
|
3227
|
-
app.post("/api/config/providers/:provider/test", async (c) => {
|
|
3228
|
-
const provider = c.req.param("provider");
|
|
3229
|
-
const body = await readJson(c.req.raw);
|
|
3230
|
-
if (!body.ok) {
|
|
3231
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3232
|
-
}
|
|
3233
|
-
const result = await testProviderConnection(
|
|
3234
|
-
options.configPath,
|
|
3235
|
-
provider,
|
|
3236
|
-
body.data
|
|
3237
|
-
);
|
|
3238
|
-
if (!result) {
|
|
3239
|
-
return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
|
|
3240
|
-
}
|
|
3241
|
-
return c.json(ok(result));
|
|
3242
|
-
});
|
|
3243
|
-
app.post("/api/config/providers/:provider/auth/start", async (c) => {
|
|
3244
|
-
const provider = c.req.param("provider");
|
|
3245
|
-
let payload = {};
|
|
3246
|
-
const rawBody = await c.req.raw.text();
|
|
3247
|
-
if (rawBody.trim().length > 0) {
|
|
3248
|
-
try {
|
|
3249
|
-
payload = JSON.parse(rawBody);
|
|
3250
|
-
} catch {
|
|
3251
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3252
|
-
}
|
|
3253
|
-
}
|
|
3254
|
-
const methodId = typeof payload.methodId === "string" ? payload.methodId.trim() : void 0;
|
|
3255
|
-
try {
|
|
3256
|
-
const result = await startProviderAuth(options.configPath, provider, {
|
|
3257
|
-
methodId
|
|
3258
|
-
});
|
|
3259
|
-
if (!result) {
|
|
3260
|
-
return c.json(err("NOT_SUPPORTED", `provider auth is not supported: ${provider}`), 404);
|
|
3261
|
-
}
|
|
3262
|
-
return c.json(ok(result));
|
|
3263
|
-
} catch (error) {
|
|
3264
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3265
|
-
return c.json(err("AUTH_START_FAILED", message), 400);
|
|
3266
|
-
}
|
|
3267
|
-
});
|
|
3268
|
-
app.post("/api/config/providers/:provider/auth/poll", async (c) => {
|
|
3269
|
-
const provider = c.req.param("provider");
|
|
3270
|
-
const body = await readJson(c.req.raw);
|
|
3271
|
-
if (!body.ok) {
|
|
3272
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3656
|
+
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3273
3657
|
}
|
|
3274
|
-
const
|
|
3275
|
-
if (!
|
|
3276
|
-
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);
|
|
3277
3661
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
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}`
|
|
3282
3669
|
});
|
|
3283
|
-
if (!result) {
|
|
3284
|
-
return c.json(err("
|
|
3285
|
-
}
|
|
3286
|
-
if (result.status === "authorized") {
|
|
3287
|
-
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
3670
|
+
if (!result.ok) {
|
|
3671
|
+
return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3288
3672
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
const provider = c.req.param("provider");
|
|
3293
|
-
try {
|
|
3294
|
-
const result = await importProviderAuthFromCli(options.configPath, provider);
|
|
3295
|
-
if (!result) {
|
|
3296
|
-
return c.json(err("NOT_SUPPORTED", `provider cli auth import is not supported: ${provider}`), 404);
|
|
3297
|
-
}
|
|
3298
|
-
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
3299
|
-
return c.json(ok(result));
|
|
3300
|
-
} catch (error) {
|
|
3301
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3302
|
-
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);
|
|
3303
3676
|
}
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3677
|
+
const content = await buildPluginContentView(sanitized);
|
|
3678
|
+
return c.json(ok(content));
|
|
3679
|
+
};
|
|
3680
|
+
install = async (c) => {
|
|
3307
3681
|
const body = await readJson(c.req.raw);
|
|
3308
|
-
if (!body.ok) {
|
|
3682
|
+
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
3309
3683
|
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3310
3684
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
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);
|
|
3314
3687
|
}
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
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) => {
|
|
3319
3706
|
const body = await readJson(c.req.raw);
|
|
3320
|
-
if (!body.ok) {
|
|
3707
|
+
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
3321
3708
|
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3322
3709
|
}
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
return c.json(ok(result));
|
|
3326
|
-
});
|
|
3327
|
-
app.get("/api/chat/capabilities", async (c) => {
|
|
3328
|
-
const chatRuntime = options.chatRuntime;
|
|
3329
|
-
if (!chatRuntime) {
|
|
3330
|
-
return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
|
|
3331
|
-
}
|
|
3332
|
-
const query = c.req.query();
|
|
3333
|
-
const params = {
|
|
3334
|
-
sessionKey: readNonEmptyString(query.sessionKey),
|
|
3335
|
-
agentId: readNonEmptyString(query.agentId)
|
|
3336
|
-
};
|
|
3337
|
-
try {
|
|
3338
|
-
const capabilities = chatRuntime.getCapabilities ? await chatRuntime.getCapabilities(params) : { stopSupported: Boolean(chatRuntime.stopTurn) };
|
|
3339
|
-
return c.json(ok(capabilities));
|
|
3340
|
-
} catch (error) {
|
|
3341
|
-
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);
|
|
3342
3712
|
}
|
|
3343
|
-
});
|
|
3344
|
-
app.get("/api/chat/session-types", async (c) => {
|
|
3345
3713
|
try {
|
|
3346
|
-
const payload = await
|
|
3714
|
+
const payload = await manageMarketplacePlugin({
|
|
3715
|
+
options: this.options,
|
|
3716
|
+
body: body.data
|
|
3717
|
+
});
|
|
3347
3718
|
return c.json(ok(payload));
|
|
3348
3719
|
} catch (error) {
|
|
3349
|
-
|
|
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);
|
|
3350
3728
|
}
|
|
3351
|
-
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
...option.required === true ? { required: true } : {}
|
|
3365
|
-
}))
|
|
3366
|
-
} : {}
|
|
3367
|
-
}));
|
|
3368
|
-
const payload = {
|
|
3369
|
-
commands,
|
|
3370
|
-
total: commands.length
|
|
3371
|
-
};
|
|
3372
|
-
return c.json(ok(payload));
|
|
3373
|
-
} catch (error) {
|
|
3374
|
-
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);
|
|
3375
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
|
|
3376
3771
|
});
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
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);
|
|
3384
3825
|
}
|
|
3385
|
-
const
|
|
3386
|
-
|
|
3387
|
-
|
|
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
|
+
);
|
|
3388
3833
|
}
|
|
3389
|
-
const
|
|
3390
|
-
const
|
|
3391
|
-
const
|
|
3392
|
-
const
|
|
3393
|
-
const
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
};
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
});
|
|
3414
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3415
|
-
return c.json(ok(response));
|
|
3416
|
-
} catch (error) {
|
|
3417
|
-
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);
|
|
3418
3858
|
}
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
const
|
|
3422
|
-
if (
|
|
3423
|
-
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
|
+
);
|
|
3424
3867
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
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);
|
|
3428
3870
|
}
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
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);
|
|
3432
3881
|
}
|
|
3433
|
-
const
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
} catch (error) {
|
|
3442
|
-
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
|
+
);
|
|
3443
3890
|
}
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
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);
|
|
3449
3900
|
}
|
|
3901
|
+
return c.json(ok(contentResult.data));
|
|
3902
|
+
};
|
|
3903
|
+
install = async (c) => {
|
|
3450
3904
|
const body = await readJson(c.req.raw);
|
|
3451
|
-
if (!body.ok) {
|
|
3905
|
+
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
3452
3906
|
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3453
3907
|
}
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
return c.json(err("INVALID_BODY", "message is required"), 400);
|
|
3457
|
-
}
|
|
3458
|
-
const sessionKey = readNonEmptyString(body.data.sessionKey) ?? `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
|
|
3459
|
-
const requestedAt = /* @__PURE__ */ new Date();
|
|
3460
|
-
const startedAtMs = requestedAt.getTime();
|
|
3461
|
-
const metadata = isRecord(body.data.metadata) ? body.data.metadata : void 0;
|
|
3462
|
-
const requestedAgentId = readNonEmptyString(body.data.agentId) ?? resolveAgentIdFromSessionKey(sessionKey);
|
|
3463
|
-
const requestedModel = readNonEmptyString(body.data.model);
|
|
3464
|
-
let runId = createChatRunId();
|
|
3465
|
-
const supportsManagedRuns = Boolean(chatRuntime.startTurnRun && chatRuntime.streamRun);
|
|
3466
|
-
let stopCapabilities = { stopSupported: Boolean(chatRuntime.stopTurn) };
|
|
3467
|
-
if (chatRuntime.getCapabilities) {
|
|
3468
|
-
try {
|
|
3469
|
-
stopCapabilities = await chatRuntime.getCapabilities({
|
|
3470
|
-
sessionKey,
|
|
3471
|
-
...requestedAgentId ? { agentId: requestedAgentId } : {}
|
|
3472
|
-
});
|
|
3473
|
-
} catch {
|
|
3474
|
-
stopCapabilities = {
|
|
3475
|
-
stopSupported: false,
|
|
3476
|
-
stopReason: "failed to resolve runtime stop capability"
|
|
3477
|
-
};
|
|
3478
|
-
}
|
|
3479
|
-
}
|
|
3480
|
-
const request = {
|
|
3481
|
-
message,
|
|
3482
|
-
sessionKey,
|
|
3483
|
-
channel: readNonEmptyString(body.data.channel) ?? "ui",
|
|
3484
|
-
chatId: readNonEmptyString(body.data.chatId) ?? "web-ui",
|
|
3485
|
-
runId,
|
|
3486
|
-
...requestedAgentId ? { agentId: requestedAgentId } : {},
|
|
3487
|
-
...requestedModel ? { model: requestedModel } : {},
|
|
3488
|
-
...metadata ? { metadata } : {}
|
|
3489
|
-
};
|
|
3490
|
-
let managedRun = null;
|
|
3491
|
-
if (supportsManagedRuns && chatRuntime.startTurnRun) {
|
|
3492
|
-
try {
|
|
3493
|
-
managedRun = await chatRuntime.startTurnRun(request);
|
|
3494
|
-
} catch (error) {
|
|
3495
|
-
return c.json(err("CHAT_TURN_FAILED", formatUserFacingError(error)), 500);
|
|
3496
|
-
}
|
|
3497
|
-
if (readNonEmptyString(managedRun.runId)) {
|
|
3498
|
-
runId = readNonEmptyString(managedRun.runId);
|
|
3499
|
-
}
|
|
3500
|
-
stopCapabilities = {
|
|
3501
|
-
stopSupported: managedRun.stopSupported,
|
|
3502
|
-
...readNonEmptyString(managedRun.stopReason) ? { stopReason: readNonEmptyString(managedRun.stopReason) } : {}
|
|
3503
|
-
};
|
|
3504
|
-
}
|
|
3505
|
-
const encoder = new TextEncoder();
|
|
3506
|
-
const stream = new ReadableStream({
|
|
3507
|
-
start: async (controller) => {
|
|
3508
|
-
const push = (event, data) => {
|
|
3509
|
-
controller.enqueue(encoder.encode(toSseFrame(event, data)));
|
|
3510
|
-
};
|
|
3511
|
-
try {
|
|
3512
|
-
push("ready", {
|
|
3513
|
-
sessionKey: managedRun?.sessionKey ?? sessionKey,
|
|
3514
|
-
requestedAt: managedRun?.requestedAt ?? requestedAt.toISOString(),
|
|
3515
|
-
runId,
|
|
3516
|
-
stopSupported: stopCapabilities.stopSupported,
|
|
3517
|
-
...readNonEmptyString(stopCapabilities.stopReason) ? { stopReason: readNonEmptyString(stopCapabilities.stopReason) } : {}
|
|
3518
|
-
});
|
|
3519
|
-
if (supportsManagedRuns && chatRuntime.streamRun) {
|
|
3520
|
-
let hasFinal2 = false;
|
|
3521
|
-
for await (const event of chatRuntime.streamRun({ runId })) {
|
|
3522
|
-
const typed = event;
|
|
3523
|
-
if (typed.type === "delta") {
|
|
3524
|
-
if (typed.delta) {
|
|
3525
|
-
push("delta", { delta: typed.delta });
|
|
3526
|
-
}
|
|
3527
|
-
continue;
|
|
3528
|
-
}
|
|
3529
|
-
if (typed.type === "session_event") {
|
|
3530
|
-
push("session_event", typed.event);
|
|
3531
|
-
continue;
|
|
3532
|
-
}
|
|
3533
|
-
if (typed.type === "final") {
|
|
3534
|
-
const latestRun = chatRuntime.getRun ? await chatRuntime.getRun({ runId }) : null;
|
|
3535
|
-
const response = latestRun ? buildChatTurnViewFromRun({
|
|
3536
|
-
run: latestRun,
|
|
3537
|
-
fallbackSessionKey: sessionKey,
|
|
3538
|
-
fallbackAgentId: requestedAgentId,
|
|
3539
|
-
fallbackModel: requestedModel,
|
|
3540
|
-
fallbackReply: typed.result.reply
|
|
3541
|
-
}) : buildChatTurnView({
|
|
3542
|
-
result: typed.result,
|
|
3543
|
-
fallbackSessionKey: sessionKey,
|
|
3544
|
-
requestedAgentId,
|
|
3545
|
-
requestedModel,
|
|
3546
|
-
requestedAt,
|
|
3547
|
-
startedAtMs
|
|
3548
|
-
});
|
|
3549
|
-
hasFinal2 = true;
|
|
3550
|
-
push("final", response);
|
|
3551
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3552
|
-
continue;
|
|
3553
|
-
}
|
|
3554
|
-
if (typed.type === "error") {
|
|
3555
|
-
push("error", {
|
|
3556
|
-
code: "CHAT_TURN_FAILED",
|
|
3557
|
-
message: formatUserFacingError(typed.error)
|
|
3558
|
-
});
|
|
3559
|
-
return;
|
|
3560
|
-
}
|
|
3561
|
-
}
|
|
3562
|
-
if (!hasFinal2) {
|
|
3563
|
-
push("error", {
|
|
3564
|
-
code: "CHAT_TURN_FAILED",
|
|
3565
|
-
message: "stream ended without a final result"
|
|
3566
|
-
});
|
|
3567
|
-
return;
|
|
3568
|
-
}
|
|
3569
|
-
push("done", { ok: true });
|
|
3570
|
-
return;
|
|
3571
|
-
}
|
|
3572
|
-
const streamTurn = chatRuntime.processTurnStream;
|
|
3573
|
-
if (!streamTurn) {
|
|
3574
|
-
const result = await chatRuntime.processTurn(request);
|
|
3575
|
-
const response = buildChatTurnView({
|
|
3576
|
-
result,
|
|
3577
|
-
fallbackSessionKey: sessionKey,
|
|
3578
|
-
requestedAgentId,
|
|
3579
|
-
requestedModel,
|
|
3580
|
-
requestedAt,
|
|
3581
|
-
startedAtMs
|
|
3582
|
-
});
|
|
3583
|
-
push("final", response);
|
|
3584
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3585
|
-
push("done", { ok: true });
|
|
3586
|
-
return;
|
|
3587
|
-
}
|
|
3588
|
-
let hasFinal = false;
|
|
3589
|
-
for await (const event of streamTurn(request)) {
|
|
3590
|
-
const typed = event;
|
|
3591
|
-
if (typed.type === "delta") {
|
|
3592
|
-
if (typed.delta) {
|
|
3593
|
-
push("delta", { delta: typed.delta });
|
|
3594
|
-
}
|
|
3595
|
-
continue;
|
|
3596
|
-
}
|
|
3597
|
-
if (typed.type === "session_event") {
|
|
3598
|
-
push("session_event", typed.event);
|
|
3599
|
-
continue;
|
|
3600
|
-
}
|
|
3601
|
-
if (typed.type === "final") {
|
|
3602
|
-
const response = buildChatTurnView({
|
|
3603
|
-
result: typed.result,
|
|
3604
|
-
fallbackSessionKey: sessionKey,
|
|
3605
|
-
requestedAgentId,
|
|
3606
|
-
requestedModel,
|
|
3607
|
-
requestedAt,
|
|
3608
|
-
startedAtMs
|
|
3609
|
-
});
|
|
3610
|
-
hasFinal = true;
|
|
3611
|
-
push("final", response);
|
|
3612
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3613
|
-
continue;
|
|
3614
|
-
}
|
|
3615
|
-
if (typed.type === "error") {
|
|
3616
|
-
push("error", {
|
|
3617
|
-
code: "CHAT_TURN_FAILED",
|
|
3618
|
-
message: formatUserFacingError(typed.error)
|
|
3619
|
-
});
|
|
3620
|
-
return;
|
|
3621
|
-
}
|
|
3622
|
-
}
|
|
3623
|
-
if (!hasFinal) {
|
|
3624
|
-
push("error", {
|
|
3625
|
-
code: "CHAT_TURN_FAILED",
|
|
3626
|
-
message: "stream ended without a final result"
|
|
3627
|
-
});
|
|
3628
|
-
return;
|
|
3629
|
-
}
|
|
3630
|
-
push("done", { ok: true });
|
|
3631
|
-
} catch (error) {
|
|
3632
|
-
push("error", {
|
|
3633
|
-
code: "CHAT_TURN_FAILED",
|
|
3634
|
-
message: formatUserFacingError(error)
|
|
3635
|
-
});
|
|
3636
|
-
} finally {
|
|
3637
|
-
controller.close();
|
|
3638
|
-
}
|
|
3639
|
-
}
|
|
3640
|
-
});
|
|
3641
|
-
return new Response(stream, {
|
|
3642
|
-
status: 200,
|
|
3643
|
-
headers: {
|
|
3644
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
3645
|
-
"Cache-Control": "no-cache, no-transform",
|
|
3646
|
-
"Connection": "keep-alive",
|
|
3647
|
-
"X-Accel-Buffering": "no"
|
|
3648
|
-
}
|
|
3649
|
-
});
|
|
3650
|
-
});
|
|
3651
|
-
app.get("/api/chat/runs", async (c) => {
|
|
3652
|
-
const chatRuntime = options.chatRuntime;
|
|
3653
|
-
if (!chatRuntime?.listRuns) {
|
|
3654
|
-
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);
|
|
3655
3910
|
}
|
|
3656
|
-
const query = c.req.query();
|
|
3657
|
-
const sessionKey = readNonEmptyString(query.sessionKey);
|
|
3658
|
-
const states = readChatRunStates(query.states);
|
|
3659
|
-
const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
|
|
3660
3911
|
try {
|
|
3661
|
-
const
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
...Number.isFinite(limit) ? { limit } : {}
|
|
3912
|
+
const payload = await installMarketplaceSkill({
|
|
3913
|
+
options: this.options,
|
|
3914
|
+
body: body.data
|
|
3665
3915
|
});
|
|
3666
|
-
return c.json(ok(
|
|
3916
|
+
return c.json(ok(payload));
|
|
3667
3917
|
} catch (error) {
|
|
3668
|
-
|
|
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);
|
|
3669
3926
|
}
|
|
3670
|
-
}
|
|
3671
|
-
|
|
3672
|
-
const
|
|
3673
|
-
if (!
|
|
3674
|
-
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);
|
|
3675
3932
|
}
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
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);
|
|
3679
3935
|
}
|
|
3680
3936
|
try {
|
|
3681
|
-
const
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
}
|
|
3685
|
-
return c.json(ok(
|
|
3937
|
+
const payload = await manageMarketplaceSkill({
|
|
3938
|
+
options: this.options,
|
|
3939
|
+
body: body.data
|
|
3940
|
+
});
|
|
3941
|
+
return c.json(ok(payload));
|
|
3686
3942
|
} catch (error) {
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
return c.json(err("NOT_AVAILABLE", "chat run stream unavailable"), 503);
|
|
3696
|
-
}
|
|
3697
|
-
const runId = readNonEmptyString(c.req.param("runId"));
|
|
3698
|
-
if (!runId) {
|
|
3699
|
-
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);
|
|
3700
3951
|
}
|
|
3952
|
+
};
|
|
3953
|
+
getRecommendations = async (c) => {
|
|
3701
3954
|
const query = c.req.query();
|
|
3702
|
-
const
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
const stream = new ReadableStream({
|
|
3709
|
-
start: async (controller) => {
|
|
3710
|
-
const push = (event, data) => {
|
|
3711
|
-
controller.enqueue(encoder.encode(toSseFrame(event, data)));
|
|
3712
|
-
};
|
|
3713
|
-
try {
|
|
3714
|
-
push("ready", {
|
|
3715
|
-
sessionKey: run.sessionKey,
|
|
3716
|
-
requestedAt: run.requestedAt,
|
|
3717
|
-
runId: run.runId,
|
|
3718
|
-
stopSupported: run.stopSupported,
|
|
3719
|
-
...readNonEmptyString(run.stopReason) ? { stopReason: readNonEmptyString(run.stopReason) } : {}
|
|
3720
|
-
});
|
|
3721
|
-
let hasFinal = false;
|
|
3722
|
-
for await (const event of streamRun({
|
|
3723
|
-
runId: run.runId,
|
|
3724
|
-
...Number.isFinite(fromEventIndex) ? { fromEventIndex } : {}
|
|
3725
|
-
})) {
|
|
3726
|
-
const typed = event;
|
|
3727
|
-
if (typed.type === "delta") {
|
|
3728
|
-
if (typed.delta) {
|
|
3729
|
-
push("delta", { delta: typed.delta });
|
|
3730
|
-
}
|
|
3731
|
-
continue;
|
|
3732
|
-
}
|
|
3733
|
-
if (typed.type === "session_event") {
|
|
3734
|
-
push("session_event", typed.event);
|
|
3735
|
-
continue;
|
|
3736
|
-
}
|
|
3737
|
-
if (typed.type === "final") {
|
|
3738
|
-
const latestRun = await getRun({ runId: run.runId });
|
|
3739
|
-
const response = latestRun ? buildChatTurnViewFromRun({
|
|
3740
|
-
run: latestRun,
|
|
3741
|
-
fallbackSessionKey: run.sessionKey,
|
|
3742
|
-
fallbackAgentId: run.agentId,
|
|
3743
|
-
fallbackModel: run.model,
|
|
3744
|
-
fallbackReply: typed.result.reply
|
|
3745
|
-
}) : buildChatTurnView({
|
|
3746
|
-
result: typed.result,
|
|
3747
|
-
fallbackSessionKey: run.sessionKey,
|
|
3748
|
-
requestedAgentId: run.agentId,
|
|
3749
|
-
requestedModel: run.model,
|
|
3750
|
-
requestedAt: new Date(run.requestedAt),
|
|
3751
|
-
startedAtMs: Date.parse(run.requestedAt)
|
|
3752
|
-
});
|
|
3753
|
-
hasFinal = true;
|
|
3754
|
-
push("final", response);
|
|
3755
|
-
continue;
|
|
3756
|
-
}
|
|
3757
|
-
if (typed.type === "error") {
|
|
3758
|
-
push("error", {
|
|
3759
|
-
code: "CHAT_TURN_FAILED",
|
|
3760
|
-
message: formatUserFacingError(typed.error)
|
|
3761
|
-
});
|
|
3762
|
-
return;
|
|
3763
|
-
}
|
|
3764
|
-
}
|
|
3765
|
-
if (!hasFinal) {
|
|
3766
|
-
const latestRun = await getRun({ runId: run.runId });
|
|
3767
|
-
if (latestRun?.state === "failed") {
|
|
3768
|
-
push("error", {
|
|
3769
|
-
code: "CHAT_TURN_FAILED",
|
|
3770
|
-
message: formatUserFacingError(latestRun.error ?? "chat run failed")
|
|
3771
|
-
});
|
|
3772
|
-
return;
|
|
3773
|
-
}
|
|
3774
|
-
}
|
|
3775
|
-
push("done", { ok: true });
|
|
3776
|
-
} catch (error) {
|
|
3777
|
-
push("error", {
|
|
3778
|
-
code: "CHAT_TURN_FAILED",
|
|
3779
|
-
message: formatUserFacingError(error)
|
|
3780
|
-
});
|
|
3781
|
-
} finally {
|
|
3782
|
-
controller.close();
|
|
3783
|
-
}
|
|
3784
|
-
}
|
|
3785
|
-
});
|
|
3786
|
-
return new Response(stream, {
|
|
3787
|
-
status: 200,
|
|
3788
|
-
headers: {
|
|
3789
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
3790
|
-
"Cache-Control": "no-cache, no-transform",
|
|
3791
|
-
"Connection": "keep-alive",
|
|
3792
|
-
"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
|
|
3793
3961
|
}
|
|
3794
3962
|
});
|
|
3795
|
-
|
|
3796
|
-
|
|
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) => {
|
|
3797
3982
|
const query = c.req.query();
|
|
3798
3983
|
const q = typeof query.q === "string" ? query.q : void 0;
|
|
3799
3984
|
const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
|
|
3800
3985
|
const activeMinutes = typeof query.activeMinutes === "string" ? Number.parseInt(query.activeMinutes, 10) : void 0;
|
|
3801
|
-
const data = listSessions(options.configPath, {
|
|
3986
|
+
const data = listSessions(this.options.configPath, {
|
|
3802
3987
|
q,
|
|
3803
3988
|
limit: Number.isFinite(limit) ? limit : void 0,
|
|
3804
3989
|
activeMinutes: Number.isFinite(activeMinutes) ? activeMinutes : void 0
|
|
3805
3990
|
});
|
|
3806
3991
|
return c.json(ok(data));
|
|
3807
|
-
}
|
|
3808
|
-
|
|
3992
|
+
};
|
|
3993
|
+
getSessionHistory = (c) => {
|
|
3809
3994
|
const key = decodeURIComponent(c.req.param("key"));
|
|
3810
3995
|
const query = c.req.query();
|
|
3811
3996
|
const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
|
|
3812
|
-
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);
|
|
3813
3998
|
if (!data) {
|
|
3814
3999
|
return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
|
|
3815
4000
|
}
|
|
3816
4001
|
return c.json(ok(data));
|
|
3817
|
-
}
|
|
3818
|
-
|
|
4002
|
+
};
|
|
4003
|
+
patchSession = async (c) => {
|
|
3819
4004
|
const key = decodeURIComponent(c.req.param("key"));
|
|
3820
4005
|
const body = await readJson(c.req.raw);
|
|
3821
4006
|
if (!body.ok || !body.data || typeof body.data !== "object") {
|
|
@@ -3823,12 +4008,12 @@ function createUiRouter(options) {
|
|
|
3823
4008
|
}
|
|
3824
4009
|
let availableSessionTypes;
|
|
3825
4010
|
if (Object.prototype.hasOwnProperty.call(body.data, "sessionType")) {
|
|
3826
|
-
const sessionTypes = await buildChatSessionTypesView(options.chatRuntime);
|
|
4011
|
+
const sessionTypes = await buildChatSessionTypesView(this.options.chatRuntime);
|
|
3827
4012
|
availableSessionTypes = sessionTypes.options.map((item) => item.value);
|
|
3828
4013
|
}
|
|
3829
4014
|
let data;
|
|
3830
4015
|
try {
|
|
3831
|
-
data = patchSession(options.configPath, key, body.data, {
|
|
4016
|
+
data = patchSession(this.options.configPath, key, body.data, {
|
|
3832
4017
|
...availableSessionTypes ? { availableSessionTypes } : {}
|
|
3833
4018
|
});
|
|
3834
4019
|
} catch (error) {
|
|
@@ -3843,111 +4028,81 @@ function createUiRouter(options) {
|
|
|
3843
4028
|
if (!data) {
|
|
3844
4029
|
return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
|
|
3845
4030
|
}
|
|
3846
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
4031
|
+
this.options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3847
4032
|
return c.json(ok(data));
|
|
3848
|
-
}
|
|
3849
|
-
|
|
4033
|
+
};
|
|
4034
|
+
deleteSession = (c) => {
|
|
3850
4035
|
const key = decodeURIComponent(c.req.param("key"));
|
|
3851
|
-
const deleted = deleteSession(options.configPath, key);
|
|
4036
|
+
const deleted = deleteSession(this.options.configPath, key);
|
|
3852
4037
|
if (!deleted) {
|
|
3853
4038
|
return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
|
|
3854
4039
|
}
|
|
3855
|
-
options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3856
|
-
return c.json(ok({ deleted: true }));
|
|
3857
|
-
});
|
|
3858
|
-
app.get("/api/cron", (c) => {
|
|
3859
|
-
if (!options.cronService) {
|
|
3860
|
-
return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
|
|
3861
|
-
}
|
|
3862
|
-
const query = c.req.query();
|
|
3863
|
-
const includeDisabled = query.all === "1" || query.all === "true" || query.all === "yes";
|
|
3864
|
-
const jobs = options.cronService.listJobs(includeDisabled).map((job) => buildCronJobView(job));
|
|
3865
|
-
return c.json(ok({ jobs, total: jobs.length }));
|
|
3866
|
-
});
|
|
3867
|
-
app.delete("/api/cron/:id", (c) => {
|
|
3868
|
-
if (!options.cronService) {
|
|
3869
|
-
return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
|
|
3870
|
-
}
|
|
3871
|
-
const id = decodeURIComponent(c.req.param("id"));
|
|
3872
|
-
const deleted = options.cronService.removeJob(id);
|
|
3873
|
-
if (!deleted) {
|
|
3874
|
-
return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
|
|
3875
|
-
}
|
|
4040
|
+
this.options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3876
4041
|
return c.json(ok({ deleted: true }));
|
|
3877
|
-
}
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
app.
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
app.
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
app.post("/api/
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
if (!body.ok) {
|
|
3942
|
-
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3943
|
-
}
|
|
3944
|
-
const result = await executeConfigAction(options.configPath, actionId, body.data ?? {});
|
|
3945
|
-
if (!result.ok) {
|
|
3946
|
-
return c.json(err(result.code, result.message, result.details), 400);
|
|
3947
|
-
}
|
|
3948
|
-
return c.json(ok(result.data));
|
|
3949
|
-
});
|
|
3950
|
-
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);
|
|
3951
4106
|
return app;
|
|
3952
4107
|
}
|
|
3953
4108
|
|