@artflo-ai/artflo-openclaw-plugin 0.0.3 → 0.0.5

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 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
@@ -36,7 +36,7 @@ const plugin = {
36
36
  console.log(`[artflo] API Key not configured. Skipping initialization.`);
37
37
  const sessions = createSessionRegistryService(config);
38
38
  api.registerService(sessions.service);
39
- registerArtfloTools(api, { config, sessions: sessions.registry });
39
+ registerArtfloTools(api, { config, workspaceDir: api.rootDir, sessions: sessions.registry });
40
40
  return;
41
41
  }
42
42
  // ── 自动获取 appKey / vipAppId / vipGroup ─────────────────────────
@@ -66,8 +66,23 @@ const plugin = {
66
66
  api.registerService(sessions.service);
67
67
  registerArtfloTools(api, {
68
68
  config,
69
+ workspaceDir: api.rootDir,
69
70
  sessions: sessions.registry,
70
71
  });
72
+ // ── 每次 prompt 构建时注入 Artflo 配置状态 ──────────────────────
73
+ api.on('before_prompt_build', () => {
74
+ const masked = config.apiKey
75
+ ? `****${config.apiKey.slice(-4)}`
76
+ : '未配置';
77
+ return {
78
+ appendSystemContext: [
79
+ `[Artflo Plugin] env=${config.env}, apiKey=${masked}`,
80
+ config.apiKey
81
+ ? 'API Key 已配置,可直接使用画布功能。'
82
+ : 'API Key 未配置,请引导用户通过 artflo_set_api_key 设置。',
83
+ ].join(' | '),
84
+ };
85
+ });
71
86
  },
72
87
  };
73
88
  export default plugin;
@@ -29,6 +29,7 @@ export const ARTFLO_TOOL_NAMES = {
29
29
  waitForCompletion: 'artflo_canvas_wait_for_completion',
30
30
  executePlan: 'artflo_canvas_execute_plan',
31
31
  createCanvas: 'artflo_canvas_create',
32
+ getLastCanvas: 'artflo_canvas_get_last',
32
33
  listModels: 'artflo_canvas_list_models',
33
34
  uploadFile: 'artflo_upload_file',
34
35
  setApiKey: 'artflo_set_api_key',
@@ -1,12 +1,2 @@
1
- /** HTTP API base URL (trailing slash stripped). */
2
- export function getApiBaseUrl(config) {
3
- return config.apiBaseUrl.replace(/\/+$/, '');
4
- }
5
- /**
6
- * Derive the WebSocket URL from apiBaseUrl.
7
- * https://webapi.artflo.ai → wss://webapi.artflo.ai/canvas/ws
8
- */
9
- export function getCanvasWsUrl(config) {
10
- const hostname = new URL(config.apiBaseUrl).hostname;
11
- return `wss://${hostname}/canvas/ws`;
12
- }
1
+ // Re-export from api-paths for backward compatibility
2
+ export { buildApiUrl, buildWsUrl, buildVipUrl, buildWebUrl } from './api-paths.js';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * All Artflo API paths in one place.
3
+ * Every HTTP/WS request in this plugin MUST use a path from here.
4
+ * Do NOT construct API URLs manually elsewhere.
5
+ */
6
+ /** WebSocket */
7
+ export const WS_CANVAS = '/canvas/ws';
8
+ /** apiBaseUrl paths */
9
+ export const API_CREATE_CANVAS = '/canvas';
10
+ export const API_UPLOAD_FILE = '/storage/obs/starii';
11
+ /** vipApiBaseUrl paths */
12
+ export const API_VIP_INFO = '/h5/user/vip_info_by_group.json';
13
+ /** webApiBaseUrl paths */
14
+ export const API_CLIENT_PARAMS = '/api/subscribe-client-params';
15
+ // ── Helper builders ─────────────────────────────────────────────────
16
+ function strip(url) {
17
+ return url.replace(/\/+$/, '');
18
+ }
19
+ export function buildApiUrl(config, path) {
20
+ return `${strip(config.apiBaseUrl)}${path}`;
21
+ }
22
+ export function buildVipUrl(config, path) {
23
+ return `${strip(config.vipApiBaseUrl)}${path}`;
24
+ }
25
+ export function buildWebUrl(config, path) {
26
+ return `${strip(config.webApiBaseUrl)}${path}`;
27
+ }
28
+ export function buildWsUrl(config) {
29
+ const hostname = new URL(config.apiBaseUrl).hostname;
30
+ return `wss://${hostname}${WS_CANVAS}`;
31
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { basename } from 'node:path';
10
- import { getApiBaseUrl } from './api-base.js';
10
+ import { buildApiUrl, API_UPLOAD_FILE } from '../api/api-paths.js';
11
11
  /**
12
12
  * Upload a local file or a remote URL to Artflo OBS.
13
13
  *
@@ -37,8 +37,7 @@ export async function uploadFile(config, source, filename) {
37
37
  }
38
38
  const formData = new FormData();
39
39
  formData.append('file', fileBlob, resolvedFilename);
40
- const baseUrl = getApiBaseUrl(config);
41
- const response = await fetch(`${baseUrl}/storage/obs/starii`, {
40
+ const response = await fetch(buildApiUrl(config, API_UPLOAD_FILE), {
42
41
  method: 'POST',
43
42
  headers: {
44
43
  'X-API-Key': config.apiKey,
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import WebSocket from 'ws';
3
- import { getCanvasWsUrl } from '../api/api-base.js';
3
+ import { buildWsUrl } from '../api/api-paths.js';
4
4
  import { writeWsTrace } from './ws-trace.js';
5
5
  import { WS_ERROR, isJoinErrorMessage, isJoinSuccessMessage, isPushErrorMessage, isPushSuccessMessage, isSyncPushMessage, } from './types.js';
6
6
  export class CanvasWebSocketClient extends EventEmitter {
@@ -33,7 +33,7 @@ export class CanvasWebSocketClient extends EventEmitter {
33
33
  queryParams.set('country_code', params.countryCode || this.config.countryCode);
34
34
  queryParams.set('api_key', params.apiKey || this.config.apiKey);
35
35
  queryParams.set('canvas_id', params.canvasId);
36
- return `${getCanvasWsUrl(this.config)}?${queryParams.toString()}`;
36
+ return `${buildWsUrl(this.config)}?${queryParams.toString()}`;
37
37
  }
38
38
  async connect(params) {
39
39
  if (this.connected) {
@@ -1,14 +1,9 @@
1
1
  /**
2
2
  * Create a new Artflo canvas via the REST API.
3
- *
4
- * Derives the HTTP API base URL from the configured WebSocket URL:
5
- * wss://prewebapi.artflo.ai/canvas/ws → https://prewebapi.artflo.ai
6
- * wss://webapi.artflo.ai/canvas/ws → https://webapi.artflo.ai
7
3
  */
8
- import { getApiBaseUrl } from '../api/api-base.js';
4
+ import { buildApiUrl, buildWebUrl, API_CREATE_CANVAS } from '../api/api-paths.js';
9
5
  export async function createCanvas(config, name = 'Untitled') {
10
- const baseUrl = getApiBaseUrl(config);
11
- const response = await fetch(`${baseUrl}/canvas`, {
6
+ const response = await fetch(buildApiUrl(config, API_CREATE_CANVAS), {
12
7
  method: 'POST',
13
8
  headers: {
14
9
  'Content-Type': 'application/json',
@@ -24,14 +19,9 @@ export async function createCanvas(config, name = 'Untitled') {
24
19
  throw new Error(`Create canvas failed: code=${body.code}`);
25
20
  }
26
21
  const canvasId = body.data.id;
27
- // Derive project URL from API host: prewebapi → pre.artflo.ai, webapi → artflo.ai
28
- const hostname = new URL(baseUrl).hostname;
29
- const projectHost = hostname.startsWith('prewebapi')
30
- ? 'pre.artflo.ai'
31
- : 'artflo.ai';
32
22
  return {
33
23
  id: canvasId,
34
24
  name: body.data.name ?? name,
35
- url: `https://${projectHost}/project/${canvasId}`,
25
+ url: `${buildWebUrl(config, '/project/' + canvasId)}`,
36
26
  };
37
27
  }
@@ -2,8 +2,9 @@
2
2
  * Fetch appKey, vipAppId, vipGroup from the subscribe-client-params API.
3
3
  * GET {webApiBaseUrl}/api/subscribe-client-params
4
4
  */
5
+ import { API_CLIENT_PARAMS } from '../api/api-paths.js';
5
6
  export async function fetchClientParams(webApiBaseUrl) {
6
- const url = `${webApiBaseUrl.replace(/\/+$/, '')}/api/subscribe-client-params`;
7
+ const url = `${webApiBaseUrl.replace(/\/+$/, '')}${API_CLIENT_PARAMS}`;
7
8
  const response = await fetch(url);
8
9
  if (!response.ok) {
9
10
  throw new Error(`fetchClientParams failed: HTTP ${response.status}`);
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Fetch user VIP / subscription status from the Artflo subscription API.
3
3
  */
4
+ import { buildVipUrl, API_VIP_INFO } from '../api/api-paths.js';
4
5
  export async function fetchVipInfo(config) {
5
- const url = new URL(`${config.vipApiBaseUrl}/h5/user/vip_info_by_group.json`);
6
+ const url = new URL(buildVipUrl(config, API_VIP_INFO));
6
7
  url.searchParams.set('app_id', config.vipAppId);
7
8
  url.searchParams.set('vip_group', config.vipGroup);
8
9
  const response = await fetch(url.toString(), {
@@ -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
+ }
@@ -22,6 +22,7 @@ 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,
@@ -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',
@@ -559,17 +584,30 @@ export function registerArtfloTools(api, context) {
559
584
  });
560
585
  api.registerTool({
561
586
  name: ARTFLO_TOOL_NAMES.setApiKey,
562
- label: 'Artflo Set API Key',
563
- description: 'Save the user\'s Artflo API Key to the plugin config and restart the gateway. Call this when the user provides their API Key in chat. After success, the gateway will restart and all Artflo tools will be functional.',
587
+ label: 'Artflo Set Config',
588
+ 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
589
  parameters: Type.Object({
565
- apiKey: Type.String({ minLength: 1, description: 'The Artflo API Key provided by the user.' }),
590
+ apiKey: Type.Optional(Type.String({ minLength: 1, description: 'Artflo API Key.' })),
591
+ env: Type.Optional(Type.String({ enum: ['release', 'test'], description: 'Environment: release or test.' })),
566
592
  }),
567
593
  async execute(_id, params) {
568
- const { apiKey } = params;
594
+ const { apiKey, env } = params;
595
+ if (!apiKey && !env) {
596
+ return jsonResult({
597
+ ok: false,
598
+ label: ARTFLO_TOOL_NAMES.setApiKey,
599
+ error: 'At least one of apiKey or env must be provided.',
600
+ });
601
+ }
569
602
  try {
570
603
  const cfg = api.runtime.config.loadConfig();
571
604
  const pluginEntry = cfg.plugins?.entries?.['artflo-openclaw-plugin'] ?? {};
572
605
  const existingConfig = pluginEntry.config ?? {};
606
+ const updatedConfig = { ...existingConfig };
607
+ if (apiKey)
608
+ updatedConfig.apiKey = apiKey;
609
+ if (env)
610
+ updatedConfig.env = env;
573
611
  const nextConfig = {
574
612
  ...cfg,
575
613
  plugins: {
@@ -579,10 +617,7 @@ export function registerArtfloTools(api, context) {
579
617
  'artflo-openclaw-plugin': {
580
618
  ...pluginEntry,
581
619
  enabled: true,
582
- config: {
583
- ...existingConfig,
584
- apiKey,
585
- },
620
+ config: updatedConfig,
586
621
  },
587
622
  },
588
623
  },
@@ -592,8 +627,8 @@ export function registerArtfloTools(api, context) {
592
627
  ok: true,
593
628
  label: ARTFLO_TOOL_NAMES.setApiKey,
594
629
  data: {
595
- message: 'API Key saved. Please restart gateway with: openclaw gateway restart',
596
- saved: true,
630
+ message: 'Config saved. Gateway will auto-reload.',
631
+ saved: { apiKey: apiKey ? true : false, env: env || null },
597
632
  },
598
633
  });
599
634
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artflo-ai/artflo-openclaw-plugin",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin that connects directly to Artflo canvas WebSocket runtime.",
6
6
  "keywords": [
@@ -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. 保存成功后,提示用户运行 `openclaw gateway restart` 重启 gateway 使配置生效。
38
- 5. 重启后即可正常使用所有画布功能。
37
+ 4. 保存成功后,gateway 会自动重新加载配置,稍等片刻即可正常使用所有画布功能。
39
38
 
40
39
  ## 操作策略
41
40
 
@@ -48,6 +47,8 @@ metadata:
48
47
  - 规划时不要臆造底层画布数字节点类型。应使用 plan 中的节点类型,例如 `input`、`refine`、`process`、`selector`、`batch`、`crop`。
49
48
  - 在假设画布运行时已就绪之前,先读取连接状态。
50
49
  - 将插件配置视为 WebSocket 凭证和连接默认值的唯一真实来源。
50
+ - **禁止编造 API URL**:不要自行拼接或猜测 Artflo API 路径。所有 API 交互必须通过插件提供的 tool 完成(如 `artflo_canvas_get_last`、`artflo_canvas_create`、`artflo_upload_file` 等)。不要使用 `fetch` 或 `curl` 直接调用 Artflo API。
51
+ - **资源保存路径**:画布生成完成后需要下载保存的资源(图片、视频等),必须保存到 `/artflo/outbound` 目录。不要使用 `/tmp` 或其他临时目录。
51
52
  - 当插件布局引擎可用时,不要手工编造节点坐标。
52
53
 
53
54
  ## 默认编辑规则
@@ -63,7 +64,7 @@ metadata:
63
64
 
64
65
  ## 推荐流程
65
66
 
66
- 1. 检查是否有可用的 canvas id。如果没有,调用 `artflo_canvas_create` 创建新画布,并将返回的 URL 分享给用户。
67
+ 1. 检查是否有可用的 canvas id。如果没有,调用 `artflo_canvas_get_last` 获取上次使用的画布,没有的话调用 `artflo_canvas_create` 创建新画布,并将返回的 URL 分享给用户。
67
68
  2. 调用 `artflo_canvas_connection_status` 检查连接状态。
68
69
  3. 如果未连接,使用目标 canvas id 调用 `artflo_canvas_connect`。
69
70
  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**: Call `artflo_canvas_create` to create a new canvas. Use the returned `canvasId` for all subsequent operations. Share the `url` with the user so they can view the canvas.
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. Show the link: `https://artflo.ai/project/{canvas_id}`.
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