@artflo-ai/artflo-openclaw-plugin 0.0.4 → 0.0.6
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/README.md +1 -0
- package/dist/index.js +23 -1
- package/dist/src/config.js +0 -1
- package/dist/src/constants.js +2 -2
- package/dist/src/core/api/api-base.js +1 -1
- package/dist/src/core/api/api-paths.js +2 -5
- package/dist/src/core/config/fetch-user-status.js +34 -0
- package/dist/src/core/config/model-config-transformer.js +4 -8
- package/dist/src/services/manifest-service.js +37 -0
- package/dist/src/tools/register-tools.js +79 -21
- package/package.json +7 -2
- package/skills/artflo-canvas/SKILL.md +5 -4
- package/skills/artflo-canvas/references/planning-guide.md +2 -2
- package/dist/src/core/config/fetch-vip-info.js +0 -31
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ are auto-detected or fetched from the Artflo API at startup.
|
|
|
72
72
|
- `artflo_canvas_wait_for_completion` — Wait for node completion
|
|
73
73
|
- `artflo_canvas_execute_plan` — Execute a structured workflow plan
|
|
74
74
|
- `artflo_canvas_create` — Create a new canvas
|
|
75
|
+
- `artflo_canvas_get_last` — Get last used canvas id
|
|
75
76
|
- `artflo_canvas_list_models` — List available models with pricing
|
|
76
77
|
- `artflo_upload_file` — Upload files to Artflo storage
|
|
77
78
|
- `artflo_set_api_key` — Save API Key to config (used in chat onboarding)
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createPluginConfig } from './src/config.js';
|
|
|
2
2
|
import { createSessionRegistryService } from './src/services/canvas-session-registry.js';
|
|
3
3
|
import { registerArtfloTools } from './src/tools/register-tools.js';
|
|
4
4
|
import { fetchClientParams } from './src/core/config/fetch-client-params.js';
|
|
5
|
+
import { fetchUserStatus } from './src/core/config/fetch-user-status.js';
|
|
5
6
|
import { PLUGIN_DESCRIPTION, PLUGIN_ID, PLUGIN_NAME } from './src/constants.js';
|
|
6
7
|
const pluginConfigSchema = {
|
|
7
8
|
type: 'object',
|
|
@@ -36,7 +37,7 @@ const plugin = {
|
|
|
36
37
|
console.log(`[artflo] API Key not configured. Skipping initialization.`);
|
|
37
38
|
const sessions = createSessionRegistryService(config);
|
|
38
39
|
api.registerService(sessions.service);
|
|
39
|
-
registerArtfloTools(api, { config, sessions: sessions.registry });
|
|
40
|
+
registerArtfloTools(api, { config, workspaceDir: api.rootDir, sessions: sessions.registry });
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
43
|
// ── 自动获取 appKey / vipAppId / vipGroup ─────────────────────────
|
|
@@ -66,8 +67,29 @@ const plugin = {
|
|
|
66
67
|
api.registerService(sessions.service);
|
|
67
68
|
registerArtfloTools(api, {
|
|
68
69
|
config,
|
|
70
|
+
workspaceDir: api.rootDir,
|
|
69
71
|
sessions: sessions.registry,
|
|
70
72
|
});
|
|
73
|
+
// ── 每次 prompt 构建时注入 Artflo 配置和用户状态 ──────────────
|
|
74
|
+
api.on('before_prompt_build', async () => {
|
|
75
|
+
const masked = config.apiKey
|
|
76
|
+
? `****${config.apiKey.slice(-4)}`
|
|
77
|
+
: '未配置';
|
|
78
|
+
const lines = [`[Artflo Plugin] env=${config.env}, apiKey=${masked}, tz=${config.timeZone}, country=${config.countryCode}`];
|
|
79
|
+
if (!config.apiKey) {
|
|
80
|
+
lines.push('API Key 未配置,请引导用户通过 artflo_set_api_key 设置。');
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
try {
|
|
84
|
+
const status = await fetchUserStatus(config);
|
|
85
|
+
lines.push(`plan=${status.plan}, subscribed=${status.isSubscribed}, credits=${status.credits}`);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
lines.push('API Key 已配置,用户状态获取失败。');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { appendSystemContext: lines.join(' | ') };
|
|
92
|
+
});
|
|
71
93
|
},
|
|
72
94
|
};
|
|
73
95
|
export default plugin;
|
package/dist/src/config.js
CHANGED
|
@@ -41,7 +41,6 @@ export function createPluginConfig(api) {
|
|
|
41
41
|
return {
|
|
42
42
|
env,
|
|
43
43
|
apiBaseUrl: urls.apiBaseUrl,
|
|
44
|
-
vipApiBaseUrl: urls.vipApiBaseUrl,
|
|
45
44
|
webApiBaseUrl: urls.webApiBaseUrl,
|
|
46
45
|
apiKey: readString(pluginConfig.apiKey, ''),
|
|
47
46
|
appKey: readString(pluginConfig.appKey, ''),
|
package/dist/src/constants.js
CHANGED
|
@@ -4,12 +4,10 @@ export const PLUGIN_DESCRIPTION = 'Expose deterministic Artflo canvas tools over
|
|
|
4
4
|
export const ENV_CONFIG = {
|
|
5
5
|
release: {
|
|
6
6
|
apiBaseUrl: 'https://webapi.artflo.ai',
|
|
7
|
-
vipApiBaseUrl: 'https://api-h5-sub.starii.com',
|
|
8
7
|
webApiBaseUrl: 'https://artflo.ai',
|
|
9
8
|
},
|
|
10
9
|
test: {
|
|
11
10
|
apiBaseUrl: 'https://testwebapi.artflo.ai',
|
|
12
|
-
vipApiBaseUrl: 'https://pre-api-h5-sub.starii.com',
|
|
13
11
|
webApiBaseUrl: 'https://test.artflo.ai',
|
|
14
12
|
},
|
|
15
13
|
};
|
|
@@ -29,7 +27,9 @@ export const ARTFLO_TOOL_NAMES = {
|
|
|
29
27
|
waitForCompletion: 'artflo_canvas_wait_for_completion',
|
|
30
28
|
executePlan: 'artflo_canvas_execute_plan',
|
|
31
29
|
createCanvas: 'artflo_canvas_create',
|
|
30
|
+
getLastCanvas: 'artflo_canvas_get_last',
|
|
32
31
|
listModels: 'artflo_canvas_list_models',
|
|
33
32
|
uploadFile: 'artflo_upload_file',
|
|
34
33
|
setApiKey: 'artflo_set_api_key',
|
|
34
|
+
getUserStatus: 'artflo_get_user_status',
|
|
35
35
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Re-export from api-paths for backward compatibility
|
|
2
|
-
export { buildApiUrl, buildWsUrl,
|
|
2
|
+
export { buildApiUrl, buildWsUrl, buildWebUrl } from './api-paths.js';
|
|
@@ -8,8 +8,8 @@ export const WS_CANVAS = '/canvas/ws';
|
|
|
8
8
|
/** apiBaseUrl paths */
|
|
9
9
|
export const API_CREATE_CANVAS = '/canvas';
|
|
10
10
|
export const API_UPLOAD_FILE = '/storage/obs/starii';
|
|
11
|
-
|
|
12
|
-
export const
|
|
11
|
+
export const API_CREDITS_REMAINING = '/user/credits_remaining';
|
|
12
|
+
export const API_SUBSCRIPTION_PLAN = '/user/subscription_plan';
|
|
13
13
|
/** webApiBaseUrl paths */
|
|
14
14
|
export const API_CLIENT_PARAMS = '/api/subscribe-client-params';
|
|
15
15
|
// ── Helper builders ─────────────────────────────────────────────────
|
|
@@ -19,9 +19,6 @@ function strip(url) {
|
|
|
19
19
|
export function buildApiUrl(config, path) {
|
|
20
20
|
return `${strip(config.apiBaseUrl)}${path}`;
|
|
21
21
|
}
|
|
22
|
-
export function buildVipUrl(config, path) {
|
|
23
|
-
return `${strip(config.vipApiBaseUrl)}${path}`;
|
|
24
|
-
}
|
|
25
22
|
export function buildWebUrl(config, path) {
|
|
26
23
|
return `${strip(config.webApiBaseUrl)}${path}`;
|
|
27
24
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch user subscription plan and credits from Artflo API.
|
|
3
|
+
* Replaces the old fetch-vip-info.ts which used the starii VIP API.
|
|
4
|
+
*/
|
|
5
|
+
import { buildApiUrl, API_CREDITS_REMAINING, API_SUBSCRIPTION_PLAN } from '../api/api-paths.js';
|
|
6
|
+
export async function fetchUserStatus(config) {
|
|
7
|
+
const headers = {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
'X-API-Key': config.apiKey,
|
|
10
|
+
};
|
|
11
|
+
const [creditsRes, planRes] = await Promise.all([
|
|
12
|
+
fetch(buildApiUrl(config, API_CREDITS_REMAINING), { headers }).catch(() => null),
|
|
13
|
+
fetch(buildApiUrl(config, API_SUBSCRIPTION_PLAN), { headers }).catch(() => null),
|
|
14
|
+
]);
|
|
15
|
+
let credits = 0;
|
|
16
|
+
if (creditsRes?.ok) {
|
|
17
|
+
const body = (await creditsRes.json());
|
|
18
|
+
if (body.code === 0 && body.data) {
|
|
19
|
+
credits = body.data.total_available ?? 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
let plan = 'free';
|
|
23
|
+
let isSubscribed = false;
|
|
24
|
+
let expireAt = 0;
|
|
25
|
+
if (planRes?.ok) {
|
|
26
|
+
const body = (await planRes.json());
|
|
27
|
+
if (body.code === 0 && body.data) {
|
|
28
|
+
plan = body.data.plan ?? 'free';
|
|
29
|
+
isSubscribed = body.data.is_valid === true;
|
|
30
|
+
expireAt = body.data.expire_at ?? 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { plan, isSubscribed, credits, expireAt };
|
|
34
|
+
}
|
|
@@ -31,11 +31,9 @@ const OUTPUT_TOOL_DESCRIPTIONS = {
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
// Transform helpers
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
|
-
function transformModelItem(model,
|
|
34
|
+
function transformModelItem(model, isSubscribed) {
|
|
35
35
|
const available = !model.disable &&
|
|
36
|
-
(!model.subscribe_available ||
|
|
37
|
-
(model.subscribe_available && isVip) ||
|
|
38
|
-
model.is_free_limit);
|
|
36
|
+
(!model.subscribe_available || isSubscribed);
|
|
39
37
|
const item = {
|
|
40
38
|
key: model.model_key,
|
|
41
39
|
name: model.model_name,
|
|
@@ -43,7 +41,6 @@ function transformModelItem(model, isVip) {
|
|
|
43
41
|
isDefault: model.is_default,
|
|
44
42
|
available,
|
|
45
43
|
requiresSubscription: model.subscribe_available,
|
|
46
|
-
isFreeLimit: model.is_free_limit,
|
|
47
44
|
};
|
|
48
45
|
if (model.support_ratio?.length)
|
|
49
46
|
item.supportedRatios = model.support_ratio;
|
|
@@ -66,14 +63,14 @@ function transformModelItem(model, isVip) {
|
|
|
66
63
|
// ---------------------------------------------------------------------------
|
|
67
64
|
// Public API
|
|
68
65
|
// ---------------------------------------------------------------------------
|
|
69
|
-
export function transformCanvasConfig(config,
|
|
66
|
+
export function transformCanvasConfig(config, isSubscribed) {
|
|
70
67
|
const categories = [];
|
|
71
68
|
for (const [toolTypeStr, info] of Object.entries(CATEGORY_INFO)) {
|
|
72
69
|
const toolType = Number(toolTypeStr);
|
|
73
70
|
const models = config[info.configKey];
|
|
74
71
|
if (!Array.isArray(models) || models.length === 0)
|
|
75
72
|
continue;
|
|
76
|
-
const transformed = models.map((m) => transformModelItem(m,
|
|
73
|
+
const transformed = models.map((m) => transformModelItem(m, isSubscribed));
|
|
77
74
|
if (transformed.some((m) => m.available)) {
|
|
78
75
|
categories.push({
|
|
79
76
|
name: info.name,
|
|
@@ -90,7 +87,6 @@ export function transformCanvasConfig(config, isVip) {
|
|
|
90
87
|
toolType: tool.tool_type,
|
|
91
88
|
name: tool.name,
|
|
92
89
|
description: OUTPUT_TOOL_DESCRIPTIONS[tool.tool_type] ?? tool.name,
|
|
93
|
-
isFreeLimit: tool.is_free_limit,
|
|
94
90
|
});
|
|
95
91
|
}
|
|
96
92
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
const ARTFLO_DIR = '.artflo';
|
|
4
|
+
const MANIFEST_FILE = 'manifest.json';
|
|
5
|
+
function getManifestPath(workspaceDir) {
|
|
6
|
+
return path.join(workspaceDir, ARTFLO_DIR, MANIFEST_FILE);
|
|
7
|
+
}
|
|
8
|
+
async function ensureArtfloDir(workspaceDir) {
|
|
9
|
+
const artfloDir = path.join(workspaceDir, ARTFLO_DIR);
|
|
10
|
+
try {
|
|
11
|
+
await fs.mkdir(artfloDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// ignore
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function readManifest(workspaceDir) {
|
|
18
|
+
const manifestPath = getManifestPath(workspaceDir);
|
|
19
|
+
try {
|
|
20
|
+
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
21
|
+
return JSON.parse(content);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function writeManifest(workspaceDir, manifest) {
|
|
28
|
+
await ensureArtfloDir(workspaceDir);
|
|
29
|
+
const manifestPath = getManifestPath(workspaceDir);
|
|
30
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
31
|
+
}
|
|
32
|
+
export async function setProjectId(workspaceDir, id) {
|
|
33
|
+
await writeManifest(workspaceDir, { project: id });
|
|
34
|
+
}
|
|
35
|
+
export async function getProjectId(workspaceDir) {
|
|
36
|
+
return (await readManifest(workspaceDir)).project;
|
|
37
|
+
}
|
|
@@ -16,12 +16,13 @@ function jsonResult(payload) {
|
|
|
16
16
|
import { ARTFLO_TOOL_NAMES } from '../constants.js';
|
|
17
17
|
import { createCanvas } from '../core/canvas/create-canvas.js';
|
|
18
18
|
import { uploadFile } from '../core/api/upload-file.js';
|
|
19
|
-
import {
|
|
19
|
+
import { fetchUserStatus } from '../core/config/fetch-user-status.js';
|
|
20
20
|
import { transformCanvasConfig } from '../core/config/model-config-transformer.js';
|
|
21
21
|
import { executePlan as runExecutePlan } from '../core/executor/execute-plan.js';
|
|
22
22
|
import { ExecutionTraceWriter } from '../core/executor/execution-trace.js';
|
|
23
23
|
import { parsePlanV2, validateAndFixPlan } from '../core/plan/validate-plan.js';
|
|
24
24
|
import { writeToolTrace } from './tool-trace.js';
|
|
25
|
+
import { getProjectId, setProjectId } from '../services/manifest-service.js';
|
|
25
26
|
export function registerArtfloTools(api, context) {
|
|
26
27
|
api.registerTool({
|
|
27
28
|
name: ARTFLO_TOOL_NAMES.getNode,
|
|
@@ -152,9 +153,9 @@ export function registerArtfloTools(api, context) {
|
|
|
152
153
|
const session = context.sessions.getOrCreate(canvasId);
|
|
153
154
|
await session.connect({ canvasId });
|
|
154
155
|
const canvasConfig = session.getCanvasConfig();
|
|
155
|
-
const
|
|
156
|
+
const userStatus = await fetchUserStatus(context.config);
|
|
156
157
|
const llmConfig = canvasConfig
|
|
157
|
-
? transformCanvasConfig(canvasConfig,
|
|
158
|
+
? transformCanvasConfig(canvasConfig, userStatus.isSubscribed)
|
|
158
159
|
: null;
|
|
159
160
|
return jsonResult({
|
|
160
161
|
ok: true,
|
|
@@ -162,9 +163,9 @@ export function registerArtfloTools(api, context) {
|
|
|
162
163
|
data: {
|
|
163
164
|
canvasId,
|
|
164
165
|
subscription: {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
plan: userStatus.plan,
|
|
167
|
+
isSubscribed: userStatus.isSubscribed,
|
|
168
|
+
credits: userStatus.credits,
|
|
168
169
|
},
|
|
169
170
|
models: llmConfig?.categories ?? [],
|
|
170
171
|
outputTools: llmConfig?.outputTools ?? [],
|
|
@@ -417,6 +418,9 @@ export function registerArtfloTools(api, context) {
|
|
|
417
418
|
return runWithToolTrace(ARTFLO_TOOL_NAMES.createCanvas, params, async () => {
|
|
418
419
|
const name = params.name || 'Untitled';
|
|
419
420
|
const result = await createCanvas(context.config, name);
|
|
421
|
+
if (context.workspaceDir) {
|
|
422
|
+
await setProjectId(context.workspaceDir, result.id);
|
|
423
|
+
}
|
|
420
424
|
return jsonResult({
|
|
421
425
|
ok: true,
|
|
422
426
|
label: ARTFLO_TOOL_NAMES.createCanvas,
|
|
@@ -429,6 +433,27 @@ export function registerArtfloTools(api, context) {
|
|
|
429
433
|
});
|
|
430
434
|
},
|
|
431
435
|
});
|
|
436
|
+
api.registerTool({
|
|
437
|
+
name: ARTFLO_TOOL_NAMES.getLastCanvas,
|
|
438
|
+
label: 'Artflo Canvas Get Last',
|
|
439
|
+
description: 'Get the last used canvas id. Use this when user requests last used canvas.',
|
|
440
|
+
parameters: Type.Object({}),
|
|
441
|
+
async execute(_id, params) {
|
|
442
|
+
return runWithToolTrace(ARTFLO_TOOL_NAMES.getLastCanvas, params, async () => {
|
|
443
|
+
let canvasId = '';
|
|
444
|
+
if (context.workspaceDir) {
|
|
445
|
+
canvasId = (await getProjectId(context.workspaceDir)) ?? '';
|
|
446
|
+
}
|
|
447
|
+
return jsonResult({
|
|
448
|
+
ok: true,
|
|
449
|
+
label: ARTFLO_TOOL_NAMES.createCanvas,
|
|
450
|
+
data: {
|
|
451
|
+
canvasId: canvasId,
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
});
|
|
432
457
|
api.registerTool({
|
|
433
458
|
name: ARTFLO_TOOL_NAMES.listModels,
|
|
434
459
|
label: 'Artflo Canvas List Models',
|
|
@@ -445,7 +470,7 @@ export function registerArtfloTools(api, context) {
|
|
|
445
470
|
const session = context.sessions.getOrCreate(payload.canvasId);
|
|
446
471
|
await session.connect({ canvasId: payload.canvasId });
|
|
447
472
|
const canvasConfig = session.getCanvasConfig();
|
|
448
|
-
const
|
|
473
|
+
const userStatus = await fetchUserStatus(context.config);
|
|
449
474
|
if (!canvasConfig) {
|
|
450
475
|
return jsonResult({
|
|
451
476
|
ok: false,
|
|
@@ -474,7 +499,7 @@ export function registerArtfloTools(api, context) {
|
|
|
474
499
|
const isVideo = VIDEO_TOOL_TYPES.has(info.toolType);
|
|
475
500
|
const mapped = models.map((m) => {
|
|
476
501
|
const modelAvailable = !m.disable &&
|
|
477
|
-
(!m.subscribe_available ||
|
|
502
|
+
(!m.subscribe_available || userStatus.isSubscribed);
|
|
478
503
|
// Build per-resolution pricing
|
|
479
504
|
const resolutionPricing = m.multi_config_price && Object.keys(m.multi_config_price).length > 0
|
|
480
505
|
? Object.fromEntries(Object.entries(m.multi_config_price).map(([key, price]) => {
|
|
@@ -519,11 +544,12 @@ export function registerArtfloTools(api, context) {
|
|
|
519
544
|
label: ARTFLO_TOOL_NAMES.listModels,
|
|
520
545
|
data: {
|
|
521
546
|
subscription: {
|
|
522
|
-
|
|
523
|
-
|
|
547
|
+
plan: userStatus.plan,
|
|
548
|
+
isSubscribed: userStatus.isSubscribed,
|
|
549
|
+
credits: userStatus.credits,
|
|
524
550
|
pricingUrl: 'https://artflo.ai/pricing',
|
|
525
551
|
},
|
|
526
|
-
hint:
|
|
552
|
+
hint: userStatus.isSubscribed
|
|
527
553
|
? undefined
|
|
528
554
|
: 'User is not subscribed. Models or resolutions marked requiresSubscription need a subscription. Visit https://artflo.ai/pricing to subscribe, or use free models.',
|
|
529
555
|
categories: result,
|
|
@@ -557,19 +583,54 @@ export function registerArtfloTools(api, context) {
|
|
|
557
583
|
});
|
|
558
584
|
},
|
|
559
585
|
});
|
|
586
|
+
api.registerTool({
|
|
587
|
+
name: ARTFLO_TOOL_NAMES.getUserStatus,
|
|
588
|
+
label: 'Artflo Get User Status',
|
|
589
|
+
description: 'Get the current user subscription plan and remaining credits. Does not require a canvas connection.',
|
|
590
|
+
parameters: Type.Object({}),
|
|
591
|
+
async execute(_id, params) {
|
|
592
|
+
return runWithToolTrace(ARTFLO_TOOL_NAMES.getUserStatus, params, async () => {
|
|
593
|
+
const status = await fetchUserStatus(context.config);
|
|
594
|
+
return jsonResult({
|
|
595
|
+
ok: true,
|
|
596
|
+
label: ARTFLO_TOOL_NAMES.getUserStatus,
|
|
597
|
+
data: {
|
|
598
|
+
plan: status.plan,
|
|
599
|
+
isSubscribed: status.isSubscribed,
|
|
600
|
+
credits: status.credits,
|
|
601
|
+
expireAt: status.expireAt,
|
|
602
|
+
pricingUrl: 'https://artflo.ai/pricing',
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
},
|
|
607
|
+
});
|
|
560
608
|
api.registerTool({
|
|
561
609
|
name: ARTFLO_TOOL_NAMES.setApiKey,
|
|
562
|
-
label: 'Artflo Set
|
|
563
|
-
description: 'Save
|
|
610
|
+
label: 'Artflo Set Config',
|
|
611
|
+
description: 'Save Artflo plugin config (apiKey and/or env). Call this when the user provides their API Key or wants to switch environment. Both parameters are optional — only provided values will be updated.',
|
|
564
612
|
parameters: Type.Object({
|
|
565
|
-
apiKey: Type.String({ minLength: 1, description: '
|
|
613
|
+
apiKey: Type.Optional(Type.String({ minLength: 1, description: 'Artflo API Key.' })),
|
|
614
|
+
env: Type.Optional(Type.String({ enum: ['release', 'test'], description: 'Environment: release or test.' })),
|
|
566
615
|
}),
|
|
567
616
|
async execute(_id, params) {
|
|
568
|
-
const { apiKey } = params;
|
|
617
|
+
const { apiKey, env } = params;
|
|
618
|
+
if (!apiKey && !env) {
|
|
619
|
+
return jsonResult({
|
|
620
|
+
ok: false,
|
|
621
|
+
label: ARTFLO_TOOL_NAMES.setApiKey,
|
|
622
|
+
error: 'At least one of apiKey or env must be provided.',
|
|
623
|
+
});
|
|
624
|
+
}
|
|
569
625
|
try {
|
|
570
626
|
const cfg = api.runtime.config.loadConfig();
|
|
571
627
|
const pluginEntry = cfg.plugins?.entries?.['artflo-openclaw-plugin'] ?? {};
|
|
572
628
|
const existingConfig = pluginEntry.config ?? {};
|
|
629
|
+
const updatedConfig = { ...existingConfig };
|
|
630
|
+
if (apiKey)
|
|
631
|
+
updatedConfig.apiKey = apiKey;
|
|
632
|
+
if (env)
|
|
633
|
+
updatedConfig.env = env;
|
|
573
634
|
const nextConfig = {
|
|
574
635
|
...cfg,
|
|
575
636
|
plugins: {
|
|
@@ -579,10 +640,7 @@ export function registerArtfloTools(api, context) {
|
|
|
579
640
|
'artflo-openclaw-plugin': {
|
|
580
641
|
...pluginEntry,
|
|
581
642
|
enabled: true,
|
|
582
|
-
config:
|
|
583
|
-
...existingConfig,
|
|
584
|
-
apiKey,
|
|
585
|
-
},
|
|
643
|
+
config: updatedConfig,
|
|
586
644
|
},
|
|
587
645
|
},
|
|
588
646
|
},
|
|
@@ -592,8 +650,8 @@ export function registerArtfloTools(api, context) {
|
|
|
592
650
|
ok: true,
|
|
593
651
|
label: ARTFLO_TOOL_NAMES.setApiKey,
|
|
594
652
|
data: {
|
|
595
|
-
message: '
|
|
596
|
-
saved: true,
|
|
653
|
+
message: 'Config saved. Gateway will auto-reload.',
|
|
654
|
+
saved: { apiKey: apiKey ? true : false, env: env || null },
|
|
597
655
|
},
|
|
598
656
|
});
|
|
599
657
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@artflo-ai/artflo-openclaw-plugin",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin that connects directly to Artflo canvas WebSocket runtime.",
|
|
6
6
|
"keywords": [
|
|
@@ -20,7 +20,12 @@
|
|
|
20
20
|
"openclaw": {
|
|
21
21
|
"extensions": [
|
|
22
22
|
"./dist/index.js"
|
|
23
|
-
]
|
|
23
|
+
],
|
|
24
|
+
"install": {
|
|
25
|
+
"npmSpec": "@artflo-ai/artflo-openclaw-plugin",
|
|
26
|
+
"localPath": "extensions/artflo-openclaw-plugin",
|
|
27
|
+
"defaultChoice": "npm"
|
|
28
|
+
}
|
|
24
29
|
},
|
|
25
30
|
"publishConfig": {
|
|
26
31
|
"access": "public"
|
|
@@ -34,8 +34,7 @@ metadata:
|
|
|
34
34
|
> 请登录 [artflo.ai](https://artflo.ai),在设置页面获取 API Key,然后直接发给我,我帮你配置好。
|
|
35
35
|
|
|
36
36
|
3. 当用户发来 API Key 后,调用 `artflo_set_api_key` 工具保存到配置中。
|
|
37
|
-
4.
|
|
38
|
-
5. 重启后即可正常使用所有画布功能。
|
|
37
|
+
4. 保存成功后,gateway 会自动重新加载配置,稍等片刻即可正常使用所有画布功能。
|
|
39
38
|
|
|
40
39
|
## 操作策略
|
|
41
40
|
|
|
@@ -48,7 +47,9 @@ metadata:
|
|
|
48
47
|
- 规划时不要臆造底层画布数字节点类型。应使用 plan 中的节点类型,例如 `input`、`refine`、`process`、`selector`、`batch`、`crop`。
|
|
49
48
|
- 在假设画布运行时已就绪之前,先读取连接状态。
|
|
50
49
|
- 将插件配置视为 WebSocket 凭证和连接默认值的唯一真实来源。
|
|
51
|
-
- **禁止编造 API URL**:不要自行拼接或猜测 Artflo API 路径。所有 API 交互必须通过插件提供的 tool 完成(如 `artflo_canvas_create`、`artflo_upload_file` 等)。不要使用 `fetch` 或 `curl` 直接调用 Artflo API。
|
|
50
|
+
- **禁止编造 API URL**:不要自行拼接或猜测 Artflo API 路径。所有 API 交互必须通过插件提供的 tool 完成(如 `artflo_canvas_get_last`、`artflo_canvas_create`、`artflo_upload_file` 等)。不要使用 `fetch` 或 `curl` 直接调用 Artflo API。
|
|
51
|
+
- **model_key 保密**:`model_key`(如 `Praline_2`、`ToffeeV1-Lite`)是内部标识,仅供构建 plan 使用。向用户展示模型时只使用 `model_name`(显示名称),绝不向用户暴露 `model_key`。
|
|
52
|
+
- **资源保存路径**:画布生成完成后需要下载保存的资源(图片、视频等),必须保存到 `/artflo/outbound` 目录。不要使用 `/tmp` 或其他临时目录。
|
|
52
53
|
- 当插件布局引擎可用时,不要手工编造节点坐标。
|
|
53
54
|
|
|
54
55
|
## 默认编辑规则
|
|
@@ -64,7 +65,7 @@ metadata:
|
|
|
64
65
|
|
|
65
66
|
## 推荐流程
|
|
66
67
|
|
|
67
|
-
1. 检查是否有可用的 canvas id。如果没有,调用 `artflo_canvas_create` 创建新画布,并将返回的 URL 分享给用户。
|
|
68
|
+
1. 检查是否有可用的 canvas id。如果没有,调用 `artflo_canvas_get_last` 获取上次使用的画布,没有的话调用 `artflo_canvas_create` 创建新画布,并将返回的 URL 分享给用户。
|
|
68
69
|
2. 调用 `artflo_canvas_connection_status` 检查连接状态。
|
|
69
70
|
3. 如果未连接,使用目标 canvas id 调用 `artflo_canvas_connect`。
|
|
70
71
|
4. **调用 `artflo_canvas_get_config` 获取可用模型列表、订阅状态和 Prompt Refine 分类。** 或者调用 `artflo_canvas_list_models` 获取更详细的模型信息(含分辨率价格、订阅要求等),适合需要向用户展示模型选择时使用。
|
|
@@ -7,8 +7,8 @@ Read `references/node-schema.json` first for node type constraints, then use thi
|
|
|
7
7
|
|
|
8
8
|
Before executing any workflow, ensure you have a canvas:
|
|
9
9
|
|
|
10
|
-
1. **No canvas id available**:
|
|
11
|
-
2. **Canvas id available**: Use the existing canvas. On the first task of each day, ask the user whether to create a new canvas or reuse the existing one. The canvas URL is returned by `artflo_canvas_create`.
|
|
10
|
+
1. **No canvas id available**: First try `artflo_canvas_get_last` to retrieve the user's most recent canvas. If found, use its `canvasId` for subsequent operations. Only call `artflo_canvas_create` if `artflo_canvas_get_last` returns no canvas or fails. Share the canvas `url` with the user.
|
|
11
|
+
2. **Canvas id available**: Use the existing canvas. On the first task of each day, ask the user whether to create a new canvas or reuse the existing one. The canvas URL is returned by `artflo_canvas_create` or `artflo_canvas_get_last`.
|
|
12
12
|
3. **User requests new canvas**: Call `artflo_canvas_create` immediately.
|
|
13
13
|
|
|
14
14
|
## Core Principle
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fetch user VIP / subscription status from the Artflo subscription API.
|
|
3
|
-
*/
|
|
4
|
-
import { buildVipUrl, API_VIP_INFO } from '../api/api-paths.js';
|
|
5
|
-
export async function fetchVipInfo(config) {
|
|
6
|
-
const url = new URL(buildVipUrl(config, API_VIP_INFO));
|
|
7
|
-
url.searchParams.set('app_id', config.vipAppId);
|
|
8
|
-
url.searchParams.set('vip_group', config.vipGroup);
|
|
9
|
-
const response = await fetch(url.toString(), {
|
|
10
|
-
method: 'GET',
|
|
11
|
-
headers: {
|
|
12
|
-
'X-API-Key': config.apiKey,
|
|
13
|
-
'app_id': config.vipAppId,
|
|
14
|
-
'country_code': config.countryCode,
|
|
15
|
-
'platform': '4',
|
|
16
|
-
'system_type': '2',
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
if (!response.ok) {
|
|
20
|
-
return { isVip: false, membershipLevel: 'Free', membershipName: 'Free' };
|
|
21
|
-
}
|
|
22
|
-
const body = (await response.json());
|
|
23
|
-
if (body.code !== 0 || !body.data) {
|
|
24
|
-
return { isVip: false, membershipLevel: 'Free', membershipName: 'Free' };
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
isVip: body.data.is_vip === true || body.data.use_vip === true,
|
|
28
|
-
membershipLevel: body.data.membership?.level_name ?? 'Free',
|
|
29
|
-
membershipName: body.data.membership?.display_name ?? 'Free',
|
|
30
|
-
};
|
|
31
|
-
}
|