@nextclaw/server 0.6.2 → 0.6.4
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 +46 -1
- package/dist/index.js +436 -30
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ type ApiResponse<T> = {
|
|
|
14
14
|
ok: false;
|
|
15
15
|
error: ApiError;
|
|
16
16
|
};
|
|
17
|
+
type AppMetaView = {
|
|
18
|
+
name: string;
|
|
19
|
+
productVersion: string;
|
|
20
|
+
};
|
|
17
21
|
type ProviderConfigView = {
|
|
18
22
|
displayName?: string;
|
|
19
23
|
apiKeySet: boolean;
|
|
@@ -50,6 +54,31 @@ type ProviderConnectionTestResult = {
|
|
|
50
54
|
latencyMs: number;
|
|
51
55
|
message: string;
|
|
52
56
|
};
|
|
57
|
+
type ProviderAuthStartResult = {
|
|
58
|
+
provider: string;
|
|
59
|
+
kind: "device_code";
|
|
60
|
+
sessionId: string;
|
|
61
|
+
verificationUri: string;
|
|
62
|
+
userCode: string;
|
|
63
|
+
expiresAt: string;
|
|
64
|
+
intervalMs: number;
|
|
65
|
+
note?: string;
|
|
66
|
+
};
|
|
67
|
+
type ProviderAuthPollRequest = {
|
|
68
|
+
sessionId: string;
|
|
69
|
+
};
|
|
70
|
+
type ProviderAuthPollResult = {
|
|
71
|
+
provider: string;
|
|
72
|
+
status: "pending" | "authorized" | "denied" | "expired" | "error";
|
|
73
|
+
message?: string;
|
|
74
|
+
nextPollMs?: number;
|
|
75
|
+
};
|
|
76
|
+
type ProviderAuthImportResult = {
|
|
77
|
+
provider: string;
|
|
78
|
+
status: "imported";
|
|
79
|
+
source: "cli";
|
|
80
|
+
expiresAt?: string;
|
|
81
|
+
};
|
|
53
82
|
type AgentProfileView = {
|
|
54
83
|
id: string;
|
|
55
84
|
default?: boolean;
|
|
@@ -361,6 +390,20 @@ type ProviderSpecView = {
|
|
|
361
390
|
isGateway?: boolean;
|
|
362
391
|
isLocal?: boolean;
|
|
363
392
|
defaultApiBase?: string;
|
|
393
|
+
logo?: string;
|
|
394
|
+
apiBaseHelp?: {
|
|
395
|
+
en?: string;
|
|
396
|
+
zh?: string;
|
|
397
|
+
};
|
|
398
|
+
auth?: {
|
|
399
|
+
kind: "device_code";
|
|
400
|
+
displayName?: string;
|
|
401
|
+
note?: {
|
|
402
|
+
en?: string;
|
|
403
|
+
zh?: string;
|
|
404
|
+
};
|
|
405
|
+
supportsCliImport?: boolean;
|
|
406
|
+
};
|
|
364
407
|
defaultModels?: string[];
|
|
365
408
|
supportsWireApi?: boolean;
|
|
366
409
|
wireApiOptions?: Array<"auto" | "chat" | "responses">;
|
|
@@ -653,6 +696,7 @@ type UiServerOptions = {
|
|
|
653
696
|
host: string;
|
|
654
697
|
port: number;
|
|
655
698
|
configPath: string;
|
|
699
|
+
productVersion?: string;
|
|
656
700
|
corsOrigins?: string[] | "*";
|
|
657
701
|
staticDir?: string;
|
|
658
702
|
marketplace?: MarketplaceApiConfig;
|
|
@@ -670,6 +714,7 @@ declare function startUiServer(options: UiServerOptions): UiServerHandle;
|
|
|
670
714
|
|
|
671
715
|
type UiRouterOptions = {
|
|
672
716
|
configPath: string;
|
|
717
|
+
productVersion?: string;
|
|
673
718
|
publish: (event: UiServerEvent) => void;
|
|
674
719
|
marketplace?: MarketplaceApiConfig;
|
|
675
720
|
cronService?: InstanceType<typeof NextclawCore.CronService>;
|
|
@@ -713,4 +758,4 @@ declare function deleteSession(configPath: string, key: string): boolean;
|
|
|
713
758
|
declare function updateRuntime(configPath: string, patch: RuntimeConfigUpdate): Pick<ConfigView, "agents" | "bindings" | "session">;
|
|
714
759
|
declare function updateSecrets(configPath: string, patch: SecretsConfigUpdate): SecretsView;
|
|
715
760
|
|
|
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 };
|
|
761
|
+
export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type AppMetaView, 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,7 +1028,336 @@ 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
|
|
1354
|
+
function buildAppMetaView(options) {
|
|
1355
|
+
const productVersion = options.productVersion?.trim();
|
|
1356
|
+
return {
|
|
1357
|
+
name: "NextClaw",
|
|
1358
|
+
productVersion: productVersion && productVersion.length > 0 ? productVersion : "0.0.0"
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1007
1361
|
var DEFAULT_MARKETPLACE_API_BASE = "https://marketplace-api.nextclaw.io";
|
|
1008
1362
|
var NEXTCLAW_PLUGIN_NPM_PREFIX = "@nextclaw/channel-plugin-";
|
|
1009
1363
|
var CLAWBAY_CHANNEL_PLUGIN_NPM_SPEC = "@clawbay/clawbay-channel";
|
|
@@ -1672,7 +2026,7 @@ async function loadLocalSkillMarkdown(options, skillName) {
|
|
|
1672
2026
|
return null;
|
|
1673
2027
|
}
|
|
1674
2028
|
try {
|
|
1675
|
-
const raw = await
|
|
2029
|
+
const raw = await readFile2(skillInfo.path, "utf-8");
|
|
1676
2030
|
return {
|
|
1677
2031
|
raw,
|
|
1678
2032
|
source: skillInfo.source
|
|
@@ -2283,6 +2637,7 @@ function createUiRouter(options) {
|
|
|
2283
2637
|
const marketplaceBaseUrl = normalizeMarketplaceBaseUrl(options);
|
|
2284
2638
|
app.notFound((c) => c.json(err("NOT_FOUND", "endpoint not found"), 404));
|
|
2285
2639
|
app.get("/api/health", (c) => c.json(ok({ status: "ok" })));
|
|
2640
|
+
app.get("/api/app/meta", (c) => c.json(ok(buildAppMetaView(options))));
|
|
2286
2641
|
app.get("/api/config", (c) => {
|
|
2287
2642
|
const config = loadConfigOrDefault(options.configPath);
|
|
2288
2643
|
return c.json(ok(buildConfigView(config)));
|
|
@@ -2370,6 +2725,56 @@ function createUiRouter(options) {
|
|
|
2370
2725
|
}
|
|
2371
2726
|
return c.json(ok(result));
|
|
2372
2727
|
});
|
|
2728
|
+
app.post("/api/config/providers/:provider/auth/start", async (c) => {
|
|
2729
|
+
const provider = c.req.param("provider");
|
|
2730
|
+
try {
|
|
2731
|
+
const result = await startProviderAuth(options.configPath, provider);
|
|
2732
|
+
if (!result) {
|
|
2733
|
+
return c.json(err("NOT_SUPPORTED", `provider auth is not supported: ${provider}`), 404);
|
|
2734
|
+
}
|
|
2735
|
+
return c.json(ok(result));
|
|
2736
|
+
} catch (error) {
|
|
2737
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2738
|
+
return c.json(err("AUTH_START_FAILED", message), 400);
|
|
2739
|
+
}
|
|
2740
|
+
});
|
|
2741
|
+
app.post("/api/config/providers/:provider/auth/poll", async (c) => {
|
|
2742
|
+
const provider = c.req.param("provider");
|
|
2743
|
+
const body = await readJson(c.req.raw);
|
|
2744
|
+
if (!body.ok) {
|
|
2745
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
2746
|
+
}
|
|
2747
|
+
const sessionId = typeof body.data.sessionId === "string" ? body.data.sessionId.trim() : "";
|
|
2748
|
+
if (!sessionId) {
|
|
2749
|
+
return c.json(err("INVALID_BODY", "sessionId is required"), 400);
|
|
2750
|
+
}
|
|
2751
|
+
const result = await pollProviderAuth({
|
|
2752
|
+
configPath: options.configPath,
|
|
2753
|
+
providerName: provider,
|
|
2754
|
+
sessionId
|
|
2755
|
+
});
|
|
2756
|
+
if (!result) {
|
|
2757
|
+
return c.json(err("NOT_FOUND", "provider auth session not found"), 404);
|
|
2758
|
+
}
|
|
2759
|
+
if (result.status === "authorized") {
|
|
2760
|
+
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
2761
|
+
}
|
|
2762
|
+
return c.json(ok(result));
|
|
2763
|
+
});
|
|
2764
|
+
app.post("/api/config/providers/:provider/auth/import-cli", async (c) => {
|
|
2765
|
+
const provider = c.req.param("provider");
|
|
2766
|
+
try {
|
|
2767
|
+
const result = await importProviderAuthFromCli(options.configPath, provider);
|
|
2768
|
+
if (!result) {
|
|
2769
|
+
return c.json(err("NOT_SUPPORTED", `provider cli auth import is not supported: ${provider}`), 404);
|
|
2770
|
+
}
|
|
2771
|
+
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
2772
|
+
return c.json(ok(result));
|
|
2773
|
+
} catch (error) {
|
|
2774
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2775
|
+
return c.json(err("AUTH_IMPORT_FAILED", message), 400);
|
|
2776
|
+
}
|
|
2777
|
+
});
|
|
2373
2778
|
app.put("/api/config/channels/:channel", async (c) => {
|
|
2374
2779
|
const channel = c.req.param("channel");
|
|
2375
2780
|
const body = await readJson(c.req.raw);
|
|
@@ -2997,6 +3402,7 @@ function startUiServer(options) {
|
|
|
2997
3402
|
"/",
|
|
2998
3403
|
createUiRouter({
|
|
2999
3404
|
configPath: options.configPath,
|
|
3405
|
+
productVersion: options.productVersion,
|
|
3000
3406
|
publish,
|
|
3001
3407
|
marketplace: options.marketplace,
|
|
3002
3408
|
cronService: options.cronService,
|
|
@@ -3013,7 +3419,7 @@ function startUiServer(options) {
|
|
|
3013
3419
|
join,
|
|
3014
3420
|
getContent: async (path) => {
|
|
3015
3421
|
try {
|
|
3016
|
-
return await
|
|
3422
|
+
return await readFile3(path);
|
|
3017
3423
|
} catch {
|
|
3018
3424
|
return null;
|
|
3019
3425
|
}
|
|
@@ -3052,9 +3458,9 @@ function startUiServer(options) {
|
|
|
3052
3458
|
host: options.host,
|
|
3053
3459
|
port: options.port,
|
|
3054
3460
|
publish,
|
|
3055
|
-
close: () => new Promise((
|
|
3461
|
+
close: () => new Promise((resolve2) => {
|
|
3056
3462
|
wss.close(() => {
|
|
3057
|
-
server.close(() =>
|
|
3463
|
+
server.close(() => resolve2());
|
|
3058
3464
|
});
|
|
3059
3465
|
})
|
|
3060
3466
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/server",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|