@nextclaw/server 0.4.1 → 0.4.2
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 +60 -2
- package/dist/index.js +188 -15
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { Config } from '@nextclaw/core';
|
|
2
|
+
import { Config, ConfigActionExecuteRequest as ConfigActionExecuteRequest$1, ConfigActionExecuteResult as ConfigActionExecuteResult$1 } from '@nextclaw/core';
|
|
3
3
|
|
|
4
4
|
type ApiError = {
|
|
5
5
|
code: string;
|
|
@@ -91,9 +91,57 @@ type ConfigUiHints = Record<string, ConfigUiHint>;
|
|
|
91
91
|
type ConfigSchemaResponse = {
|
|
92
92
|
schema: Record<string, unknown>;
|
|
93
93
|
uiHints: ConfigUiHints;
|
|
94
|
+
actions: ConfigActionManifest[];
|
|
94
95
|
version: string;
|
|
95
96
|
generatedAt: string;
|
|
96
97
|
};
|
|
98
|
+
type ConfigActionType = "httpProbe" | "oauthStart" | "webhookVerify" | "openUrl" | "copyToken";
|
|
99
|
+
type ConfigActionManifest = {
|
|
100
|
+
id: string;
|
|
101
|
+
version: string;
|
|
102
|
+
scope: string;
|
|
103
|
+
title: string;
|
|
104
|
+
description?: string;
|
|
105
|
+
type: ConfigActionType;
|
|
106
|
+
trigger: "manual" | "afterSave";
|
|
107
|
+
requires?: string[];
|
|
108
|
+
request: {
|
|
109
|
+
method: "GET" | "POST" | "PUT";
|
|
110
|
+
path: string;
|
|
111
|
+
timeoutMs?: number;
|
|
112
|
+
};
|
|
113
|
+
success?: {
|
|
114
|
+
message?: string;
|
|
115
|
+
};
|
|
116
|
+
failure?: {
|
|
117
|
+
message?: string;
|
|
118
|
+
};
|
|
119
|
+
saveBeforeRun?: boolean;
|
|
120
|
+
savePatch?: Record<string, unknown>;
|
|
121
|
+
resultMap?: Record<string, string>;
|
|
122
|
+
policy?: {
|
|
123
|
+
roles?: string[];
|
|
124
|
+
rateLimitKey?: string;
|
|
125
|
+
cooldownMs?: number;
|
|
126
|
+
audit?: boolean;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
type ConfigActionExecuteRequest = {
|
|
130
|
+
scope?: string;
|
|
131
|
+
draftConfig?: Record<string, unknown>;
|
|
132
|
+
context?: {
|
|
133
|
+
actor?: string;
|
|
134
|
+
traceId?: string;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
type ConfigActionExecuteResult = {
|
|
138
|
+
ok: boolean;
|
|
139
|
+
status: "success" | "failed";
|
|
140
|
+
message: string;
|
|
141
|
+
data?: Record<string, unknown>;
|
|
142
|
+
patch?: Record<string, unknown>;
|
|
143
|
+
nextActions?: string[];
|
|
144
|
+
};
|
|
97
145
|
type UiServerEvent = {
|
|
98
146
|
type: "config.updated";
|
|
99
147
|
payload: {
|
|
@@ -134,12 +182,22 @@ type UiRouterOptions = {
|
|
|
134
182
|
};
|
|
135
183
|
declare function createUiRouter(options: UiRouterOptions): Hono;
|
|
136
184
|
|
|
185
|
+
type ExecuteActionResult = {
|
|
186
|
+
ok: true;
|
|
187
|
+
data: ConfigActionExecuteResult$1;
|
|
188
|
+
} | {
|
|
189
|
+
ok: false;
|
|
190
|
+
code: string;
|
|
191
|
+
message: string;
|
|
192
|
+
details?: Record<string, unknown>;
|
|
193
|
+
};
|
|
137
194
|
declare function buildConfigView(config: Config): ConfigView;
|
|
138
195
|
declare function buildConfigMeta(config: Config): ConfigMetaView;
|
|
139
196
|
declare function buildConfigSchemaView(_config: Config): ConfigSchemaResponse;
|
|
197
|
+
declare function executeConfigAction(configPath: string, actionId: string, request: ConfigActionExecuteRequest$1): Promise<ExecuteActionResult>;
|
|
140
198
|
declare function loadConfigOrDefault(configPath: string): Config;
|
|
141
199
|
declare function updateModel(configPath: string, model: string): ConfigView;
|
|
142
200
|
declare function updateProvider(configPath: string, providerName: string, patch: ProviderConfigUpdate): ProviderConfigView | null;
|
|
143
201
|
declare function updateChannel(configPath: string, channelName: string, patch: Record<string, unknown>): Record<string, unknown> | null;
|
|
144
202
|
|
|
145
|
-
export { type ApiError, type ApiResponse, type ChannelSpecView, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type ProviderConfigUpdate, type ProviderConfigView, type ProviderSpecView, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createUiRouter, loadConfigOrDefault, startUiServer, updateChannel, updateModel, updateProvider };
|
|
203
|
+
export { type ApiError, type ApiResponse, type ChannelSpecView, type ConfigActionExecuteRequest, type ConfigActionExecuteResult, type ConfigActionManifest, type ConfigActionType, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type ProviderConfigUpdate, type ProviderConfigView, type ProviderSpecView, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createUiRouter, executeConfigAction, loadConfigOrDefault, startUiServer, updateChannel, updateModel, updateProvider };
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
loadConfig,
|
|
16
16
|
saveConfig,
|
|
17
17
|
ConfigSchema,
|
|
18
|
+
probeFeishu,
|
|
18
19
|
PROVIDERS,
|
|
19
20
|
buildConfigSchema,
|
|
20
21
|
findProviderByName,
|
|
@@ -80,6 +81,132 @@ function sanitizePublicConfigValue(value, prefix, hints) {
|
|
|
80
81
|
}
|
|
81
82
|
return output;
|
|
82
83
|
}
|
|
84
|
+
function isObject(value) {
|
|
85
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
function deepMerge(base, patch) {
|
|
88
|
+
if (!isObject(base) || !isObject(patch)) {
|
|
89
|
+
return patch;
|
|
90
|
+
}
|
|
91
|
+
const result = { ...base };
|
|
92
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
93
|
+
const previous = result[key];
|
|
94
|
+
result[key] = deepMerge(previous, value);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
function getPathValue(source, path) {
|
|
99
|
+
if (!source || typeof source !== "object") {
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
const segments = path.split(".");
|
|
103
|
+
let current = source;
|
|
104
|
+
for (const segment of segments) {
|
|
105
|
+
if (!current || typeof current !== "object") {
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
current = current[segment];
|
|
109
|
+
}
|
|
110
|
+
return current;
|
|
111
|
+
}
|
|
112
|
+
function setPathValue(target, path, value) {
|
|
113
|
+
const segments = path.split(".");
|
|
114
|
+
if (segments.length === 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
let current = target;
|
|
118
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
119
|
+
const segment = segments[index];
|
|
120
|
+
const next = current[segment];
|
|
121
|
+
if (!isObject(next)) {
|
|
122
|
+
current[segment] = {};
|
|
123
|
+
}
|
|
124
|
+
current = current[segment];
|
|
125
|
+
}
|
|
126
|
+
current[segments[segments.length - 1]] = value;
|
|
127
|
+
}
|
|
128
|
+
function isMissingRequiredValue(value) {
|
|
129
|
+
if (value === void 0 || value === null) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
if (typeof value === "string") {
|
|
133
|
+
return value.trim().length === 0;
|
|
134
|
+
}
|
|
135
|
+
if (Array.isArray(value)) {
|
|
136
|
+
return value.length === 0;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
function resolveRuntimeConfig(config, draftConfig) {
|
|
141
|
+
if (!draftConfig || Object.keys(draftConfig).length === 0) {
|
|
142
|
+
return config;
|
|
143
|
+
}
|
|
144
|
+
const merged = deepMerge(config, draftConfig);
|
|
145
|
+
return ConfigSchema.parse(merged);
|
|
146
|
+
}
|
|
147
|
+
function getActionById(config, actionId) {
|
|
148
|
+
const actions = buildConfigSchemaView(config).actions;
|
|
149
|
+
return actions.find((item) => item.id === actionId) ?? null;
|
|
150
|
+
}
|
|
151
|
+
function messageOrDefault(action, kind, fallback) {
|
|
152
|
+
const text = kind === "success" ? action.success?.message : action.failure?.message;
|
|
153
|
+
return text?.trim() ? text : fallback;
|
|
154
|
+
}
|
|
155
|
+
async function runFeishuVerifyAction(params) {
|
|
156
|
+
const appId = String(params.config.channels.feishu.appId ?? "").trim();
|
|
157
|
+
const appSecret = String(params.config.channels.feishu.appSecret ?? "").trim();
|
|
158
|
+
if (!appId || !appSecret) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
status: "failed",
|
|
162
|
+
message: messageOrDefault(params.action, "failure", "Verification failed: missing credentials"),
|
|
163
|
+
data: {
|
|
164
|
+
error: "missing credentials (appId, appSecret)"
|
|
165
|
+
},
|
|
166
|
+
nextActions: []
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const result = await probeFeishu(appId, appSecret);
|
|
170
|
+
if (!result.ok) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
status: "failed",
|
|
174
|
+
message: `${messageOrDefault(params.action, "failure", "Verification failed")}: ${result.error}`,
|
|
175
|
+
data: {
|
|
176
|
+
error: result.error,
|
|
177
|
+
appId: result.appId ?? appId
|
|
178
|
+
},
|
|
179
|
+
nextActions: []
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const responseData = {
|
|
183
|
+
appId: result.appId,
|
|
184
|
+
botName: result.botName ?? null,
|
|
185
|
+
botOpenId: result.botOpenId ?? null
|
|
186
|
+
};
|
|
187
|
+
const patch = {};
|
|
188
|
+
for (const [targetPath, sourcePath] of Object.entries(params.action.resultMap ?? {})) {
|
|
189
|
+
const mappedValue = sourcePath.startsWith("response.data.") ? responseData[sourcePath.slice("response.data.".length)] : void 0;
|
|
190
|
+
if (mappedValue !== void 0) {
|
|
191
|
+
setPathValue(patch, targetPath, mappedValue);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
ok: true,
|
|
196
|
+
status: "success",
|
|
197
|
+
message: messageOrDefault(
|
|
198
|
+
params.action,
|
|
199
|
+
"success",
|
|
200
|
+
"Verified. Please finish Feishu event subscription and app publishing before using."
|
|
201
|
+
),
|
|
202
|
+
data: responseData,
|
|
203
|
+
patch: Object.keys(patch).length > 0 ? patch : void 0,
|
|
204
|
+
nextActions: []
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
var ACTION_HANDLERS = {
|
|
208
|
+
"channels.feishu.verifyConnection": runFeishuVerifyAction
|
|
209
|
+
};
|
|
83
210
|
function buildUiHints(config) {
|
|
84
211
|
return buildConfigSchemaView(config).uiHints;
|
|
85
212
|
}
|
|
@@ -156,6 +283,58 @@ function buildConfigMeta(config) {
|
|
|
156
283
|
function buildConfigSchemaView(_config) {
|
|
157
284
|
return buildConfigSchema({ version: getPackageVersion() });
|
|
158
285
|
}
|
|
286
|
+
async function executeConfigAction(configPath, actionId, request) {
|
|
287
|
+
const baseConfig = loadConfigOrDefault(configPath);
|
|
288
|
+
const action = getActionById(baseConfig, actionId);
|
|
289
|
+
if (!action) {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
code: "ACTION_NOT_FOUND",
|
|
293
|
+
message: `unknown action: ${actionId}`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (request.scope && request.scope !== action.scope) {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
code: "ACTION_SCOPE_MISMATCH",
|
|
300
|
+
message: `scope mismatch: expected ${action.scope}, got ${request.scope}`,
|
|
301
|
+
details: {
|
|
302
|
+
expectedScope: action.scope,
|
|
303
|
+
requestScope: request.scope
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const runtimeConfig = resolveRuntimeConfig(baseConfig, request.draftConfig);
|
|
308
|
+
for (const requiredPath of action.requires ?? []) {
|
|
309
|
+
const requiredValue = getPathValue(runtimeConfig, requiredPath);
|
|
310
|
+
if (isMissingRequiredValue(requiredValue)) {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
code: "ACTION_PRECONDITION_FAILED",
|
|
314
|
+
message: `required field missing: ${requiredPath}`,
|
|
315
|
+
details: {
|
|
316
|
+
path: requiredPath
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const handler = ACTION_HANDLERS[action.id];
|
|
322
|
+
if (!handler) {
|
|
323
|
+
return {
|
|
324
|
+
ok: false,
|
|
325
|
+
code: "ACTION_EXECUTION_FAILED",
|
|
326
|
+
message: `action handler not found for type ${action.type}`
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const result = await handler({
|
|
330
|
+
config: runtimeConfig,
|
|
331
|
+
action
|
|
332
|
+
});
|
|
333
|
+
return {
|
|
334
|
+
ok: true,
|
|
335
|
+
data: result
|
|
336
|
+
};
|
|
337
|
+
}
|
|
159
338
|
function loadConfigOrDefault(configPath) {
|
|
160
339
|
return loadConfig(configPath);
|
|
161
340
|
}
|
|
@@ -209,7 +388,6 @@ function updateChannel(configPath, channelName, patch) {
|
|
|
209
388
|
}
|
|
210
389
|
|
|
211
390
|
// src/ui/router.ts
|
|
212
|
-
import { probeFeishu } from "@nextclaw/core";
|
|
213
391
|
function ok(data) {
|
|
214
392
|
return { ok: true, data };
|
|
215
393
|
}
|
|
@@ -275,23 +453,17 @@ function createUiRouter(options) {
|
|
|
275
453
|
options.publish({ type: "config.updated", payload: { path: `channels.${channel}` } });
|
|
276
454
|
return c.json(ok(result));
|
|
277
455
|
});
|
|
278
|
-
app.post("/api/
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
if (!
|
|
282
|
-
return c.json(err("
|
|
456
|
+
app.post("/api/config/actions/:actionId/execute", async (c) => {
|
|
457
|
+
const actionId = c.req.param("actionId");
|
|
458
|
+
const body = await readJson(c.req.raw);
|
|
459
|
+
if (!body.ok) {
|
|
460
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
283
461
|
}
|
|
284
|
-
const result = await
|
|
462
|
+
const result = await executeConfigAction(options.configPath, actionId, body.data ?? {});
|
|
285
463
|
if (!result.ok) {
|
|
286
|
-
return c.json(err(
|
|
464
|
+
return c.json(err(result.code, result.message, result.details), 400);
|
|
287
465
|
}
|
|
288
|
-
return c.json(
|
|
289
|
-
ok({
|
|
290
|
-
appId: result.appId,
|
|
291
|
-
botName: result.botName ?? null,
|
|
292
|
-
botOpenId: result.botOpenId ?? null
|
|
293
|
-
})
|
|
294
|
-
);
|
|
466
|
+
return c.json(ok(result.data));
|
|
295
467
|
});
|
|
296
468
|
return app;
|
|
297
469
|
}
|
|
@@ -388,6 +560,7 @@ export {
|
|
|
388
560
|
buildConfigSchemaView,
|
|
389
561
|
buildConfigView,
|
|
390
562
|
createUiRouter,
|
|
563
|
+
executeConfigAction,
|
|
391
564
|
loadConfigOrDefault,
|
|
392
565
|
startUiServer,
|
|
393
566
|
updateChannel,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Nextclaw UI/API server.",
|
|
6
6
|
"type": "module",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@hono/node-server": "^1.13.3",
|
|
18
18
|
"hono": "^4.6.2",
|
|
19
19
|
"ws": "^8.18.0",
|
|
20
|
-
"@nextclaw/core": "^0.6.
|
|
20
|
+
"@nextclaw/core": "^0.6.5"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^20.17.6",
|