@nextclaw/server 0.4.1 → 0.4.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 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/channels/feishu/probe", async (c) => {
279
- const config = loadConfigOrDefault(options.configPath);
280
- const feishu = config.channels.feishu;
281
- if (!feishu?.appId || !feishu?.appSecret) {
282
- return c.json(err("MISSING_CREDENTIALS", "Feishu appId/appSecret not configured"), 400);
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 probeFeishu(String(feishu.appId), String(feishu.appSecret));
462
+ const result = await executeConfigAction(options.configPath, actionId, body.data ?? {});
285
463
  if (!result.ok) {
286
- return c.json(err("PROBE_FAILED", result.error), 400);
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.1",
3
+ "version": "0.4.3",
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.1"
20
+ "@nextclaw/core": "^0.6.18"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^20.17.6",