@nextclaw/server 0.6.2 → 0.6.3
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 +40 -1
- package/dist/index.js +427 -30
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -50,6 +50,31 @@ type ProviderConnectionTestResult = {
|
|
|
50
50
|
latencyMs: number;
|
|
51
51
|
message: string;
|
|
52
52
|
};
|
|
53
|
+
type ProviderAuthStartResult = {
|
|
54
|
+
provider: string;
|
|
55
|
+
kind: "device_code";
|
|
56
|
+
sessionId: string;
|
|
57
|
+
verificationUri: string;
|
|
58
|
+
userCode: string;
|
|
59
|
+
expiresAt: string;
|
|
60
|
+
intervalMs: number;
|
|
61
|
+
note?: string;
|
|
62
|
+
};
|
|
63
|
+
type ProviderAuthPollRequest = {
|
|
64
|
+
sessionId: string;
|
|
65
|
+
};
|
|
66
|
+
type ProviderAuthPollResult = {
|
|
67
|
+
provider: string;
|
|
68
|
+
status: "pending" | "authorized" | "denied" | "expired" | "error";
|
|
69
|
+
message?: string;
|
|
70
|
+
nextPollMs?: number;
|
|
71
|
+
};
|
|
72
|
+
type ProviderAuthImportResult = {
|
|
73
|
+
provider: string;
|
|
74
|
+
status: "imported";
|
|
75
|
+
source: "cli";
|
|
76
|
+
expiresAt?: string;
|
|
77
|
+
};
|
|
53
78
|
type AgentProfileView = {
|
|
54
79
|
id: string;
|
|
55
80
|
default?: boolean;
|
|
@@ -361,6 +386,20 @@ type ProviderSpecView = {
|
|
|
361
386
|
isGateway?: boolean;
|
|
362
387
|
isLocal?: boolean;
|
|
363
388
|
defaultApiBase?: string;
|
|
389
|
+
logo?: string;
|
|
390
|
+
apiBaseHelp?: {
|
|
391
|
+
en?: string;
|
|
392
|
+
zh?: string;
|
|
393
|
+
};
|
|
394
|
+
auth?: {
|
|
395
|
+
kind: "device_code";
|
|
396
|
+
displayName?: string;
|
|
397
|
+
note?: {
|
|
398
|
+
en?: string;
|
|
399
|
+
zh?: string;
|
|
400
|
+
};
|
|
401
|
+
supportsCliImport?: boolean;
|
|
402
|
+
};
|
|
364
403
|
defaultModels?: string[];
|
|
365
404
|
supportsWireApi?: boolean;
|
|
366
405
|
wireApiOptions?: Array<"auto" | "chat" | "responses">;
|
|
@@ -713,4 +752,4 @@ declare function deleteSession(configPath: string, key: string): boolean;
|
|
|
713
752
|
declare function updateRuntime(configPath: string, patch: RuntimeConfigUpdate): Pick<ConfigView, "agents" | "bindings" | "session">;
|
|
714
753
|
declare function updateSecrets(configPath: string, patch: SecretsConfigUpdate): SecretsView;
|
|
715
754
|
|
|
716
|
-
export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type BindingPeerView, type ChannelSpecView, type ChatCapabilitiesView, type ChatRunListView, type ChatRunState, type ChatRunView, type ChatTurnRequest, type ChatTurnResult, type ChatTurnStopRequest, type ChatTurnStopResult, type ChatTurnStreamEvent, type ChatTurnView, type ConfigActionExecuteRequest, type ConfigActionExecuteResult, type ConfigActionManifest, type ConfigActionType, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type CronActionResult, type CronEnableRequest, type CronJobStateView, type CronJobView, type CronListView, type CronPayloadView, type CronRunRequest, type CronScheduleView, type MarketplaceApiConfig, type MarketplaceInstallKind, type MarketplaceInstallSkillParams, type MarketplaceInstallSpec, type MarketplaceInstalledRecord, type MarketplaceInstalledView, type MarketplaceInstaller, type MarketplaceItemSummary, type MarketplaceItemType, type MarketplaceItemView, type MarketplaceListView, type MarketplaceLocalizedTextMap, type MarketplacePluginContentView, type MarketplacePluginInstallRequest, type MarketplacePluginInstallResult, type MarketplacePluginManageAction, type MarketplacePluginManageRequest, type MarketplacePluginManageResult, type MarketplaceRecommendationView, type MarketplaceSkillContentView, type MarketplaceSkillInstallRequest, type MarketplaceSkillInstallResult, type MarketplaceSkillManageAction, type MarketplaceSkillManageRequest, type MarketplaceSkillManageResult, type MarketplaceSort, type ProviderConfigUpdate, type ProviderConfigView, type ProviderConnectionTestRequest, type ProviderConnectionTestResult, type ProviderCreateRequest, type ProviderCreateResult, type ProviderDeleteResult, type ProviderSpecView, type RuntimeConfigUpdate, type SecretProviderEnvView, type SecretProviderExecView, type SecretProviderFileView, type SecretProviderView, type SecretRefView, type SecretSourceView, type SecretsConfigUpdate, type SecretsView, type SessionConfigView, type SessionEntryView, type SessionEventView, type SessionHistoryView, type SessionMessageView, type SessionPatchUpdate, type SessionsListView, type UiChatRuntime, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createCustomProvider, createUiRouter, deleteCustomProvider, deleteSession, executeConfigAction, getSessionHistory, listSessions, loadConfigOrDefault, patchSession, startUiServer, testProviderConnection, updateChannel, updateModel, updateProvider, updateRuntime, updateSecrets };
|
|
755
|
+
export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type BindingPeerView, type ChannelSpecView, type ChatCapabilitiesView, type ChatRunListView, type ChatRunState, type ChatRunView, type ChatTurnRequest, type ChatTurnResult, type ChatTurnStopRequest, type ChatTurnStopResult, type ChatTurnStreamEvent, type ChatTurnView, type ConfigActionExecuteRequest, type ConfigActionExecuteResult, type ConfigActionManifest, type ConfigActionType, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type CronActionResult, type CronEnableRequest, type CronJobStateView, type CronJobView, type CronListView, type CronPayloadView, type CronRunRequest, type CronScheduleView, type MarketplaceApiConfig, type MarketplaceInstallKind, type MarketplaceInstallSkillParams, type MarketplaceInstallSpec, type MarketplaceInstalledRecord, type MarketplaceInstalledView, type MarketplaceInstaller, type MarketplaceItemSummary, type MarketplaceItemType, type MarketplaceItemView, type MarketplaceListView, type MarketplaceLocalizedTextMap, type MarketplacePluginContentView, type MarketplacePluginInstallRequest, type MarketplacePluginInstallResult, type MarketplacePluginManageAction, type MarketplacePluginManageRequest, type MarketplacePluginManageResult, type MarketplaceRecommendationView, type MarketplaceSkillContentView, type MarketplaceSkillInstallRequest, type MarketplaceSkillInstallResult, type MarketplaceSkillManageAction, type MarketplaceSkillManageRequest, type MarketplaceSkillManageResult, type MarketplaceSort, type ProviderAuthImportResult, type ProviderAuthPollRequest, type ProviderAuthPollResult, type ProviderAuthStartResult, type ProviderConfigUpdate, type ProviderConfigView, type ProviderConnectionTestRequest, type ProviderConnectionTestResult, type ProviderCreateRequest, type ProviderCreateResult, type ProviderDeleteResult, type ProviderSpecView, type RuntimeConfigUpdate, type SecretProviderEnvView, type SecretProviderExecView, type SecretProviderFileView, type SecretProviderView, type SecretRefView, type SecretSourceView, type SecretsConfigUpdate, type SecretsView, type SessionConfigView, type SessionEntryView, type SessionEventView, type SessionHistoryView, type SessionMessageView, type SessionPatchUpdate, type SessionsListView, type UiChatRuntime, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createCustomProvider, createUiRouter, deleteCustomProvider, deleteSession, executeConfigAction, getSessionHistory, listSessions, loadConfigOrDefault, patchSession, startUiServer, testProviderConnection, updateChannel, updateModel, updateProvider, updateRuntime, updateSecrets };
|
package/dist/index.js
CHANGED
|
@@ -5,12 +5,12 @@ import { cors } from "hono/cors";
|
|
|
5
5
|
import { serve } from "@hono/node-server";
|
|
6
6
|
import { WebSocketServer, WebSocket } from "ws";
|
|
7
7
|
import { existsSync, readFileSync } from "fs";
|
|
8
|
-
import { readFile as
|
|
8
|
+
import { readFile as readFile3, stat } from "fs/promises";
|
|
9
9
|
import { join } from "path";
|
|
10
10
|
|
|
11
11
|
// src/ui/router.ts
|
|
12
12
|
import { Hono } from "hono";
|
|
13
|
-
import { readFile } from "fs/promises";
|
|
13
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
14
14
|
import * as NextclawCore from "@nextclaw/core";
|
|
15
15
|
import { buildPluginStatusReport } from "@nextclaw/openclaw-compat";
|
|
16
16
|
|
|
@@ -21,9 +21,7 @@ import {
|
|
|
21
21
|
ConfigSchema,
|
|
22
22
|
probeFeishu,
|
|
23
23
|
LiteLLMProvider,
|
|
24
|
-
PROVIDERS,
|
|
25
24
|
buildConfigSchema,
|
|
26
|
-
findProviderByName,
|
|
27
25
|
getProviderName,
|
|
28
26
|
getPackageVersion,
|
|
29
27
|
hasSecretRef,
|
|
@@ -31,22 +29,9 @@ import {
|
|
|
31
29
|
SessionManager,
|
|
32
30
|
getWorkspacePathFromConfig
|
|
33
31
|
} from "@nextclaw/core";
|
|
32
|
+
import { findBuiltinProviderByName, listBuiltinProviders } from "@nextclaw/runtime";
|
|
34
33
|
var MASK_MIN_LENGTH = 8;
|
|
35
34
|
var EXTRA_SENSITIVE_PATH_PATTERNS = [/authorization/i, /cookie/i, /session/i, /bearer/i];
|
|
36
|
-
var PROVIDER_TEST_MODEL_FALLBACKS = {
|
|
37
|
-
nextclaw: "dashscope/qwen3.5-flash",
|
|
38
|
-
openai: "gpt-5-mini",
|
|
39
|
-
deepseek: "deepseek-chat",
|
|
40
|
-
gemini: "gemini-3-flash-preview",
|
|
41
|
-
zhipu: "glm-5",
|
|
42
|
-
dashscope: "qwen3.5-flash",
|
|
43
|
-
moonshot: "kimi-k2.5",
|
|
44
|
-
minimax: "MiniMax-M2.5",
|
|
45
|
-
groq: "llama-3.1-8b-instant",
|
|
46
|
-
openrouter: "openai/gpt-5.3-codex",
|
|
47
|
-
aihubmix: "gpt-5.3-codex",
|
|
48
|
-
anthropic: "claude-opus-4-6"
|
|
49
|
-
};
|
|
50
35
|
var PREFERRED_PROVIDER_ORDER = [
|
|
51
36
|
"nextclaw",
|
|
52
37
|
"openai",
|
|
@@ -62,7 +47,8 @@ var PREFERRED_PROVIDER_ORDER = [
|
|
|
62
47
|
var PREFERRED_PROVIDER_ORDER_INDEX = new Map(
|
|
63
48
|
PREFERRED_PROVIDER_ORDER.map((name, index) => [name, index])
|
|
64
49
|
);
|
|
65
|
-
var
|
|
50
|
+
var BUILTIN_PROVIDERS = listBuiltinProviders();
|
|
51
|
+
var BUILTIN_PROVIDER_NAMES = new Set(BUILTIN_PROVIDERS.map((spec) => spec.name));
|
|
66
52
|
var CUSTOM_PROVIDER_WIRE_API_OPTIONS = ["auto", "chat", "responses"];
|
|
67
53
|
var CUSTOM_PROVIDER_PREFIX = "custom-";
|
|
68
54
|
var PROVIDER_TEST_MAX_TOKENS = 16;
|
|
@@ -103,6 +89,33 @@ function findNextCustomProviderName(config) {
|
|
|
103
89
|
}
|
|
104
90
|
return `${CUSTOM_PROVIDER_PREFIX}${index}`;
|
|
105
91
|
}
|
|
92
|
+
function createDefaultProviderConfig(defaultWireApi = "auto") {
|
|
93
|
+
return {
|
|
94
|
+
displayName: "",
|
|
95
|
+
apiKey: "",
|
|
96
|
+
apiBase: null,
|
|
97
|
+
extraHeaders: null,
|
|
98
|
+
wireApi: defaultWireApi,
|
|
99
|
+
models: []
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function ensureProviderConfig(config, providerName) {
|
|
103
|
+
const providers = config.providers;
|
|
104
|
+
const existing = providers[providerName];
|
|
105
|
+
if (existing) {
|
|
106
|
+
return existing;
|
|
107
|
+
}
|
|
108
|
+
if (isCustomProviderName(providerName)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const spec = findBuiltinProviderByName(providerName);
|
|
112
|
+
if (!spec) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const created = createDefaultProviderConfig(spec.defaultWireApi ?? "auto");
|
|
116
|
+
providers[providerName] = created;
|
|
117
|
+
return created;
|
|
118
|
+
}
|
|
106
119
|
function clearSecretRefsByPrefix(config, pathPrefix) {
|
|
107
120
|
for (const key of Object.keys(config.secrets.refs)) {
|
|
108
121
|
if (key === pathPrefix || key.startsWith(`${pathPrefix}.`)) {
|
|
@@ -363,7 +376,7 @@ function buildConfigView(config) {
|
|
|
363
376
|
const uiHints = buildUiHints(config);
|
|
364
377
|
const providers = {};
|
|
365
378
|
for (const [name, provider] of Object.entries(config.providers)) {
|
|
366
|
-
const spec =
|
|
379
|
+
const spec = findBuiltinProviderByName(name);
|
|
367
380
|
providers[name] = toProviderView(config, provider, name, uiHints, spec);
|
|
368
381
|
}
|
|
369
382
|
return {
|
|
@@ -394,7 +407,7 @@ function clearSecretRef(config, path) {
|
|
|
394
407
|
}
|
|
395
408
|
function buildConfigMeta(config) {
|
|
396
409
|
const configProviders = config.providers;
|
|
397
|
-
const builtinProviders =
|
|
410
|
+
const builtinProviders = BUILTIN_PROVIDERS.map((spec) => {
|
|
398
411
|
const providerConfig = configProviders[spec.name];
|
|
399
412
|
return {
|
|
400
413
|
name: spec.name,
|
|
@@ -406,6 +419,14 @@ function buildConfigMeta(config) {
|
|
|
406
419
|
isGateway: spec.isGateway,
|
|
407
420
|
isLocal: spec.isLocal,
|
|
408
421
|
defaultApiBase: spec.defaultApiBase,
|
|
422
|
+
logo: spec.logo,
|
|
423
|
+
apiBaseHelp: spec.apiBaseHelp,
|
|
424
|
+
auth: spec.auth ? {
|
|
425
|
+
kind: spec.auth.kind,
|
|
426
|
+
displayName: spec.auth.displayName,
|
|
427
|
+
note: spec.auth.note,
|
|
428
|
+
supportsCliImport: Boolean(spec.auth.cliCredential)
|
|
429
|
+
} : void 0,
|
|
409
430
|
defaultModels: normalizeModelList(spec.defaultModels ?? []),
|
|
410
431
|
supportsWireApi: spec.supportsWireApi,
|
|
411
432
|
wireApiOptions: spec.wireApiOptions,
|
|
@@ -438,6 +459,9 @@ function buildConfigMeta(config) {
|
|
|
438
459
|
isGateway: false,
|
|
439
460
|
isLocal: false,
|
|
440
461
|
defaultApiBase: void 0,
|
|
462
|
+
logo: void 0,
|
|
463
|
+
apiBaseHelp: void 0,
|
|
464
|
+
auth: void 0,
|
|
441
465
|
defaultModels: [],
|
|
442
466
|
supportsWireApi: true,
|
|
443
467
|
wireApiOptions: CUSTOM_PROVIDER_WIRE_API_OPTIONS,
|
|
@@ -527,11 +551,11 @@ function updateModel(configPath, patch) {
|
|
|
527
551
|
}
|
|
528
552
|
function updateProvider(configPath, providerName, patch) {
|
|
529
553
|
const config = loadConfigOrDefault(configPath);
|
|
530
|
-
const provider = config
|
|
554
|
+
const provider = ensureProviderConfig(config, providerName);
|
|
531
555
|
if (!provider) {
|
|
532
556
|
return null;
|
|
533
557
|
}
|
|
534
|
-
const spec =
|
|
558
|
+
const spec = findBuiltinProviderByName(providerName);
|
|
535
559
|
const isCustom = isCustomProviderName(providerName);
|
|
536
560
|
if (Object.prototype.hasOwnProperty.call(patch, "displayName") && isCustom) {
|
|
537
561
|
provider.displayName = normalizeOptionalDisplayName(patch.displayName) ?? "";
|
|
@@ -653,7 +677,8 @@ function resolveTestModel(config, providerName, requestedModel, provider, spec)
|
|
|
653
677
|
if (isCustomProviderName(providerName)) {
|
|
654
678
|
return null;
|
|
655
679
|
}
|
|
656
|
-
|
|
680
|
+
const specDefaultModel = normalizeModelList(spec?.defaultModels ?? [])[0] ?? null;
|
|
681
|
+
return specDefaultModel ?? defaultModel ?? null;
|
|
657
682
|
}
|
|
658
683
|
function stringifyError(error) {
|
|
659
684
|
const raw = error instanceof Error ? error.message : String(error);
|
|
@@ -661,11 +686,11 @@ function stringifyError(error) {
|
|
|
661
686
|
}
|
|
662
687
|
async function testProviderConnection(configPath, providerName, patch) {
|
|
663
688
|
const config = loadConfigOrDefault(configPath);
|
|
664
|
-
const provider = config
|
|
689
|
+
const provider = ensureProviderConfig(config, providerName);
|
|
665
690
|
if (!provider) {
|
|
666
691
|
return null;
|
|
667
692
|
}
|
|
668
|
-
const spec =
|
|
693
|
+
const spec = findBuiltinProviderByName(providerName);
|
|
669
694
|
const hasApiKeyPatch = Object.prototype.hasOwnProperty.call(patch, "apiKey");
|
|
670
695
|
const providedApiKey = normalizeOptionalString(patch.apiKey);
|
|
671
696
|
const currentApiKey = normalizeOptionalString(provider.apiKey);
|
|
@@ -1003,6 +1028,328 @@ function updateSecrets(configPath, patch) {
|
|
|
1003
1028
|
};
|
|
1004
1029
|
}
|
|
1005
1030
|
|
|
1031
|
+
// src/ui/provider-auth.ts
|
|
1032
|
+
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
1033
|
+
import { readFile } from "fs/promises";
|
|
1034
|
+
import { homedir } from "os";
|
|
1035
|
+
import { isAbsolute, resolve } from "path";
|
|
1036
|
+
import {
|
|
1037
|
+
ConfigSchema as ConfigSchema2,
|
|
1038
|
+
loadConfig as loadConfig2,
|
|
1039
|
+
saveConfig as saveConfig2
|
|
1040
|
+
} from "@nextclaw/core";
|
|
1041
|
+
import { findBuiltinProviderByName as findBuiltinProviderByName2 } from "@nextclaw/runtime";
|
|
1042
|
+
var authSessions = /* @__PURE__ */ new Map();
|
|
1043
|
+
var DEFAULT_AUTH_INTERVAL_MS = 2e3;
|
|
1044
|
+
var MAX_AUTH_INTERVAL_MS = 1e4;
|
|
1045
|
+
function normalizePositiveInt(value, fallback) {
|
|
1046
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
1047
|
+
return fallback;
|
|
1048
|
+
}
|
|
1049
|
+
return Math.floor(value);
|
|
1050
|
+
}
|
|
1051
|
+
function toBase64Url(buffer) {
|
|
1052
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
1053
|
+
}
|
|
1054
|
+
function buildPkce() {
|
|
1055
|
+
const verifier = toBase64Url(randomBytes(48));
|
|
1056
|
+
const challenge = toBase64Url(createHash("sha256").update(verifier).digest());
|
|
1057
|
+
return { verifier, challenge };
|
|
1058
|
+
}
|
|
1059
|
+
function withTrailingSlash(value) {
|
|
1060
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
1061
|
+
}
|
|
1062
|
+
function cleanupExpiredAuthSessions(now = Date.now()) {
|
|
1063
|
+
for (const [sessionId, session] of authSessions.entries()) {
|
|
1064
|
+
if (session.expiresAtMs <= now) {
|
|
1065
|
+
authSessions.delete(sessionId);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
function resolveDeviceCodeEndpoints(baseUrl, deviceCodePath, tokenPath) {
|
|
1070
|
+
const deviceCodeEndpoint = new URL(deviceCodePath, withTrailingSlash(baseUrl)).toString();
|
|
1071
|
+
const tokenEndpoint = new URL(tokenPath, withTrailingSlash(baseUrl)).toString();
|
|
1072
|
+
return { deviceCodeEndpoint, tokenEndpoint };
|
|
1073
|
+
}
|
|
1074
|
+
function resolveAuthNote(params) {
|
|
1075
|
+
return params.zh ?? params.en;
|
|
1076
|
+
}
|
|
1077
|
+
function resolveHomePath(inputPath) {
|
|
1078
|
+
const trimmed = inputPath.trim();
|
|
1079
|
+
if (!trimmed) {
|
|
1080
|
+
return trimmed;
|
|
1081
|
+
}
|
|
1082
|
+
if (trimmed === "~") {
|
|
1083
|
+
return homedir();
|
|
1084
|
+
}
|
|
1085
|
+
if (trimmed.startsWith("~/")) {
|
|
1086
|
+
return resolve(homedir(), trimmed.slice(2));
|
|
1087
|
+
}
|
|
1088
|
+
if (isAbsolute(trimmed)) {
|
|
1089
|
+
return trimmed;
|
|
1090
|
+
}
|
|
1091
|
+
return resolve(trimmed);
|
|
1092
|
+
}
|
|
1093
|
+
function normalizeExpiresAt(value) {
|
|
1094
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
1095
|
+
return Math.floor(value);
|
|
1096
|
+
}
|
|
1097
|
+
if (typeof value === "string" && value.trim()) {
|
|
1098
|
+
const asNumber = Number(value);
|
|
1099
|
+
if (Number.isFinite(asNumber) && asNumber > 0) {
|
|
1100
|
+
return Math.floor(asNumber);
|
|
1101
|
+
}
|
|
1102
|
+
const parsedTime = Date.parse(value);
|
|
1103
|
+
if (Number.isFinite(parsedTime) && parsedTime > 0) {
|
|
1104
|
+
return parsedTime;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
function readFieldAsString(source, fieldName) {
|
|
1110
|
+
if (!fieldName) {
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
const rawValue = source[fieldName];
|
|
1114
|
+
if (typeof rawValue !== "string") {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
const trimmed = rawValue.trim();
|
|
1118
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1119
|
+
}
|
|
1120
|
+
function setProviderApiKey(params) {
|
|
1121
|
+
const config = loadConfig2(params.configPath);
|
|
1122
|
+
const providers = config.providers;
|
|
1123
|
+
if (!providers[params.provider]) {
|
|
1124
|
+
providers[params.provider] = {
|
|
1125
|
+
displayName: "",
|
|
1126
|
+
apiKey: "",
|
|
1127
|
+
apiBase: null,
|
|
1128
|
+
extraHeaders: null,
|
|
1129
|
+
wireApi: "auto",
|
|
1130
|
+
models: []
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
const target = providers[params.provider];
|
|
1134
|
+
target.apiKey = params.accessToken;
|
|
1135
|
+
if (!target.apiBase && params.defaultApiBase) {
|
|
1136
|
+
target.apiBase = params.defaultApiBase;
|
|
1137
|
+
}
|
|
1138
|
+
const next = ConfigSchema2.parse(config);
|
|
1139
|
+
saveConfig2(next, params.configPath);
|
|
1140
|
+
}
|
|
1141
|
+
async function startProviderAuth(configPath, providerName) {
|
|
1142
|
+
cleanupExpiredAuthSessions();
|
|
1143
|
+
const spec = findBuiltinProviderByName2(providerName);
|
|
1144
|
+
if (!spec?.auth || spec.auth.kind !== "device_code") {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
const { deviceCodeEndpoint, tokenEndpoint } = resolveDeviceCodeEndpoints(
|
|
1148
|
+
spec.auth.baseUrl,
|
|
1149
|
+
spec.auth.deviceCodePath,
|
|
1150
|
+
spec.auth.tokenPath
|
|
1151
|
+
);
|
|
1152
|
+
const pkce = spec.auth.usePkce ? buildPkce() : null;
|
|
1153
|
+
const body = new URLSearchParams({
|
|
1154
|
+
client_id: spec.auth.clientId,
|
|
1155
|
+
scope: spec.auth.scope
|
|
1156
|
+
});
|
|
1157
|
+
if (pkce) {
|
|
1158
|
+
body.set("code_challenge", pkce.challenge);
|
|
1159
|
+
body.set("code_challenge_method", "S256");
|
|
1160
|
+
}
|
|
1161
|
+
const response = await fetch(deviceCodeEndpoint, {
|
|
1162
|
+
method: "POST",
|
|
1163
|
+
headers: {
|
|
1164
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1165
|
+
Accept: "application/json"
|
|
1166
|
+
},
|
|
1167
|
+
body
|
|
1168
|
+
});
|
|
1169
|
+
const payload = await response.json().catch(() => ({}));
|
|
1170
|
+
if (!response.ok) {
|
|
1171
|
+
const message = payload.error_description || payload.error || response.statusText || "device code auth failed";
|
|
1172
|
+
throw new Error(message);
|
|
1173
|
+
}
|
|
1174
|
+
const deviceCode = payload.device_code?.trim() ?? "";
|
|
1175
|
+
const userCode = payload.user_code?.trim() ?? "";
|
|
1176
|
+
const verificationUri = payload.verification_uri_complete?.trim() || payload.verification_uri?.trim() || "";
|
|
1177
|
+
if (!deviceCode || !userCode || !verificationUri) {
|
|
1178
|
+
throw new Error("provider auth payload is incomplete");
|
|
1179
|
+
}
|
|
1180
|
+
const intervalMs = normalizePositiveInt(payload.interval, DEFAULT_AUTH_INTERVAL_MS / 1e3) * 1e3;
|
|
1181
|
+
const expiresInSec = normalizePositiveInt(payload.expires_in, 600);
|
|
1182
|
+
const expiresAtMs = Date.now() + expiresInSec * 1e3;
|
|
1183
|
+
const sessionId = randomUUID();
|
|
1184
|
+
authSessions.set(sessionId, {
|
|
1185
|
+
sessionId,
|
|
1186
|
+
provider: providerName,
|
|
1187
|
+
configPath,
|
|
1188
|
+
deviceCode,
|
|
1189
|
+
codeVerifier: pkce?.verifier,
|
|
1190
|
+
tokenEndpoint,
|
|
1191
|
+
clientId: spec.auth.clientId,
|
|
1192
|
+
grantType: spec.auth.grantType,
|
|
1193
|
+
expiresAtMs,
|
|
1194
|
+
intervalMs
|
|
1195
|
+
});
|
|
1196
|
+
return {
|
|
1197
|
+
provider: providerName,
|
|
1198
|
+
kind: "device_code",
|
|
1199
|
+
sessionId,
|
|
1200
|
+
verificationUri,
|
|
1201
|
+
userCode,
|
|
1202
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
1203
|
+
intervalMs,
|
|
1204
|
+
note: resolveAuthNote(spec.auth.note ?? {})
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
async function pollProviderAuth(params) {
|
|
1208
|
+
cleanupExpiredAuthSessions();
|
|
1209
|
+
const session = authSessions.get(params.sessionId);
|
|
1210
|
+
if (!session || session.provider !== params.providerName || session.configPath !== params.configPath) {
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
if (Date.now() >= session.expiresAtMs) {
|
|
1214
|
+
authSessions.delete(params.sessionId);
|
|
1215
|
+
return {
|
|
1216
|
+
provider: params.providerName,
|
|
1217
|
+
status: "expired",
|
|
1218
|
+
message: "authorization session expired"
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
const body = new URLSearchParams({
|
|
1222
|
+
grant_type: session.grantType,
|
|
1223
|
+
client_id: session.clientId,
|
|
1224
|
+
device_code: session.deviceCode
|
|
1225
|
+
});
|
|
1226
|
+
if (session.codeVerifier) {
|
|
1227
|
+
body.set("code_verifier", session.codeVerifier);
|
|
1228
|
+
}
|
|
1229
|
+
const response = await fetch(session.tokenEndpoint, {
|
|
1230
|
+
method: "POST",
|
|
1231
|
+
headers: {
|
|
1232
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1233
|
+
Accept: "application/json"
|
|
1234
|
+
},
|
|
1235
|
+
body
|
|
1236
|
+
});
|
|
1237
|
+
const payload = await response.json().catch(() => ({}));
|
|
1238
|
+
if (!response.ok) {
|
|
1239
|
+
const errorCode = payload.error?.trim().toLowerCase();
|
|
1240
|
+
if (errorCode === "authorization_pending") {
|
|
1241
|
+
return {
|
|
1242
|
+
provider: params.providerName,
|
|
1243
|
+
status: "pending",
|
|
1244
|
+
nextPollMs: session.intervalMs
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
if (errorCode === "slow_down") {
|
|
1248
|
+
const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
|
|
1249
|
+
session.intervalMs = nextPollMs;
|
|
1250
|
+
authSessions.set(params.sessionId, session);
|
|
1251
|
+
return {
|
|
1252
|
+
provider: params.providerName,
|
|
1253
|
+
status: "pending",
|
|
1254
|
+
nextPollMs
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
if (errorCode === "access_denied") {
|
|
1258
|
+
authSessions.delete(params.sessionId);
|
|
1259
|
+
return {
|
|
1260
|
+
provider: params.providerName,
|
|
1261
|
+
status: "denied",
|
|
1262
|
+
message: payload.error_description || "authorization denied"
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
if (errorCode === "expired_token") {
|
|
1266
|
+
authSessions.delete(params.sessionId);
|
|
1267
|
+
return {
|
|
1268
|
+
provider: params.providerName,
|
|
1269
|
+
status: "expired",
|
|
1270
|
+
message: payload.error_description || "authorization session expired"
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
provider: params.providerName,
|
|
1275
|
+
status: "error",
|
|
1276
|
+
message: payload.error_description || payload.error || response.statusText || "authorization failed"
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
const accessToken = payload.access_token?.trim();
|
|
1280
|
+
if (!accessToken) {
|
|
1281
|
+
return {
|
|
1282
|
+
provider: params.providerName,
|
|
1283
|
+
status: "error",
|
|
1284
|
+
message: "provider token response missing access token"
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
const spec = findBuiltinProviderByName2(params.providerName);
|
|
1288
|
+
setProviderApiKey({
|
|
1289
|
+
configPath: params.configPath,
|
|
1290
|
+
provider: params.providerName,
|
|
1291
|
+
accessToken,
|
|
1292
|
+
defaultApiBase: spec?.defaultApiBase
|
|
1293
|
+
});
|
|
1294
|
+
authSessions.delete(params.sessionId);
|
|
1295
|
+
return {
|
|
1296
|
+
provider: params.providerName,
|
|
1297
|
+
status: "authorized"
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
async function importProviderAuthFromCli(configPath, providerName) {
|
|
1301
|
+
const spec = findBuiltinProviderByName2(providerName);
|
|
1302
|
+
if (!spec?.auth || spec.auth.kind !== "device_code" || !spec.auth.cliCredential) {
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
const credentialPath = resolveHomePath(spec.auth.cliCredential.path);
|
|
1306
|
+
if (!credentialPath) {
|
|
1307
|
+
throw new Error("provider cli credential path is empty");
|
|
1308
|
+
}
|
|
1309
|
+
let rawContent = "";
|
|
1310
|
+
try {
|
|
1311
|
+
rawContent = await readFile(credentialPath, "utf8");
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1314
|
+
throw new Error(`failed to read CLI credential: ${message}`);
|
|
1315
|
+
}
|
|
1316
|
+
let payload;
|
|
1317
|
+
try {
|
|
1318
|
+
const parsed = JSON.parse(rawContent);
|
|
1319
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1320
|
+
throw new Error("credential payload is not an object");
|
|
1321
|
+
}
|
|
1322
|
+
payload = parsed;
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1325
|
+
throw new Error(`invalid CLI credential JSON: ${message}`);
|
|
1326
|
+
}
|
|
1327
|
+
const accessToken = readFieldAsString(payload, spec.auth.cliCredential.accessTokenField);
|
|
1328
|
+
if (!accessToken) {
|
|
1329
|
+
throw new Error(
|
|
1330
|
+
`CLI credential missing access token field: ${spec.auth.cliCredential.accessTokenField}`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
const expiresAtMs = normalizeExpiresAt(
|
|
1334
|
+
spec.auth.cliCredential.expiresAtField ? payload[spec.auth.cliCredential.expiresAtField] : void 0
|
|
1335
|
+
);
|
|
1336
|
+
if (typeof expiresAtMs === "number" && expiresAtMs <= Date.now()) {
|
|
1337
|
+
throw new Error("CLI credential has expired, please login again");
|
|
1338
|
+
}
|
|
1339
|
+
setProviderApiKey({
|
|
1340
|
+
configPath,
|
|
1341
|
+
provider: providerName,
|
|
1342
|
+
accessToken,
|
|
1343
|
+
defaultApiBase: spec.defaultApiBase
|
|
1344
|
+
});
|
|
1345
|
+
return {
|
|
1346
|
+
provider: providerName,
|
|
1347
|
+
status: "imported",
|
|
1348
|
+
source: "cli",
|
|
1349
|
+
expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : void 0
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1006
1353
|
// src/ui/router.ts
|
|
1007
1354
|
var DEFAULT_MARKETPLACE_API_BASE = "https://marketplace-api.nextclaw.io";
|
|
1008
1355
|
var NEXTCLAW_PLUGIN_NPM_PREFIX = "@nextclaw/channel-plugin-";
|
|
@@ -1672,7 +2019,7 @@ async function loadLocalSkillMarkdown(options, skillName) {
|
|
|
1672
2019
|
return null;
|
|
1673
2020
|
}
|
|
1674
2021
|
try {
|
|
1675
|
-
const raw = await
|
|
2022
|
+
const raw = await readFile2(skillInfo.path, "utf-8");
|
|
1676
2023
|
return {
|
|
1677
2024
|
raw,
|
|
1678
2025
|
source: skillInfo.source
|
|
@@ -2370,6 +2717,56 @@ function createUiRouter(options) {
|
|
|
2370
2717
|
}
|
|
2371
2718
|
return c.json(ok(result));
|
|
2372
2719
|
});
|
|
2720
|
+
app.post("/api/config/providers/:provider/auth/start", async (c) => {
|
|
2721
|
+
const provider = c.req.param("provider");
|
|
2722
|
+
try {
|
|
2723
|
+
const result = await startProviderAuth(options.configPath, provider);
|
|
2724
|
+
if (!result) {
|
|
2725
|
+
return c.json(err("NOT_SUPPORTED", `provider auth is not supported: ${provider}`), 404);
|
|
2726
|
+
}
|
|
2727
|
+
return c.json(ok(result));
|
|
2728
|
+
} catch (error) {
|
|
2729
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2730
|
+
return c.json(err("AUTH_START_FAILED", message), 400);
|
|
2731
|
+
}
|
|
2732
|
+
});
|
|
2733
|
+
app.post("/api/config/providers/:provider/auth/poll", async (c) => {
|
|
2734
|
+
const provider = c.req.param("provider");
|
|
2735
|
+
const body = await readJson(c.req.raw);
|
|
2736
|
+
if (!body.ok) {
|
|
2737
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2738
|
+
}
|
|
2739
|
+
const sessionId = typeof body.data.sessionId === "string" ? body.data.sessionId.trim() : "";
|
|
2740
|
+
if (!sessionId) {
|
|
2741
|
+
return c.json(err("INVALID_BODY", "sessionId is required"), 400);
|
|
2742
|
+
}
|
|
2743
|
+
const result = await pollProviderAuth({
|
|
2744
|
+
configPath: options.configPath,
|
|
2745
|
+
providerName: provider,
|
|
2746
|
+
sessionId
|
|
2747
|
+
});
|
|
2748
|
+
if (!result) {
|
|
2749
|
+
return c.json(err("NOT_FOUND", "provider auth session not found"), 404);
|
|
2750
|
+
}
|
|
2751
|
+
if (result.status === "authorized") {
|
|
2752
|
+
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
2753
|
+
}
|
|
2754
|
+
return c.json(ok(result));
|
|
2755
|
+
});
|
|
2756
|
+
app.post("/api/config/providers/:provider/auth/import-cli", async (c) => {
|
|
2757
|
+
const provider = c.req.param("provider");
|
|
2758
|
+
try {
|
|
2759
|
+
const result = await importProviderAuthFromCli(options.configPath, provider);
|
|
2760
|
+
if (!result) {
|
|
2761
|
+
return c.json(err("NOT_SUPPORTED", `provider cli auth import is not supported: ${provider}`), 404);
|
|
2762
|
+
}
|
|
2763
|
+
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
2764
|
+
return c.json(ok(result));
|
|
2765
|
+
} catch (error) {
|
|
2766
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2767
|
+
return c.json(err("AUTH_IMPORT_FAILED", message), 400);
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2373
2770
|
app.put("/api/config/channels/:channel", async (c) => {
|
|
2374
2771
|
const channel = c.req.param("channel");
|
|
2375
2772
|
const body = await readJson(c.req.raw);
|
|
@@ -3013,7 +3410,7 @@ function startUiServer(options) {
|
|
|
3013
3410
|
join,
|
|
3014
3411
|
getContent: async (path) => {
|
|
3015
3412
|
try {
|
|
3016
|
-
return await
|
|
3413
|
+
return await readFile3(path);
|
|
3017
3414
|
} catch {
|
|
3018
3415
|
return null;
|
|
3019
3416
|
}
|
|
@@ -3052,9 +3449,9 @@ function startUiServer(options) {
|
|
|
3052
3449
|
host: options.host,
|
|
3053
3450
|
port: options.port,
|
|
3054
3451
|
publish,
|
|
3055
|
-
close: () => new Promise((
|
|
3452
|
+
close: () => new Promise((resolve2) => {
|
|
3056
3453
|
wss.close(() => {
|
|
3057
|
-
server.close(() =>
|
|
3454
|
+
server.close(() => resolve2());
|
|
3058
3455
|
});
|
|
3059
3456
|
})
|
|
3060
3457
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/server",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Nextclaw UI/API server.",
|
|
6
6
|
"type": "module",
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@hono/node-server": "^1.13.3",
|
|
18
18
|
"@nextclaw/openclaw-compat": "^0.2.0",
|
|
19
|
+
"@nextclaw/runtime": "^0.1.1",
|
|
19
20
|
"hono": "^4.6.2",
|
|
20
21
|
"ws": "^8.18.0",
|
|
21
|
-
"@nextclaw/core": "^0.7.
|
|
22
|
+
"@nextclaw/core": "^0.7.1"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/node": "^20.17.6",
|