@artflo-ai/artflo-openclaw-plugin 0.0.5 → 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/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',
@@ -69,19 +70,25 @@ const plugin = {
69
70
  workspaceDir: api.rootDir,
70
71
  sessions: sessions.registry,
71
72
  });
72
- // ── 每次 prompt 构建时注入 Artflo 配置状态 ──────────────────────
73
- api.on('before_prompt_build', () => {
73
+ // ── 每次 prompt 构建时注入 Artflo 配置和用户状态 ──────────────
74
+ api.on('before_prompt_build', async () => {
74
75
  const masked = config.apiKey
75
76
  ? `****${config.apiKey.slice(-4)}`
76
77
  : '未配置';
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
- };
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(' | ') };
85
92
  });
86
93
  },
87
94
  };
@@ -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, ''),
@@ -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
  };
@@ -33,4 +31,5 @@ export const ARTFLO_TOOL_NAMES = {
33
31
  listModels: 'artflo_canvas_list_models',
34
32
  uploadFile: 'artflo_upload_file',
35
33
  setApiKey: 'artflo_set_api_key',
34
+ getUserStatus: 'artflo_get_user_status',
36
35
  };
@@ -1,2 +1,2 @@
1
1
  // Re-export from api-paths for backward compatibility
2
- export { buildApiUrl, buildWsUrl, buildVipUrl, buildWebUrl } from './api-paths.js';
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
- /** vipApiBaseUrl paths */
12
- export const API_VIP_INFO = '/h5/user/vip_info_by_group.json';
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, isVip) {
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, isVip) {
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, isVip));
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
  }
@@ -16,7 +16,7 @@ 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 { fetchVipInfo } from '../core/config/fetch-vip-info.js';
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';
@@ -153,9 +153,9 @@ export function registerArtfloTools(api, context) {
153
153
  const session = context.sessions.getOrCreate(canvasId);
154
154
  await session.connect({ canvasId });
155
155
  const canvasConfig = session.getCanvasConfig();
156
- const vipInfo = await fetchVipInfo(context.config);
156
+ const userStatus = await fetchUserStatus(context.config);
157
157
  const llmConfig = canvasConfig
158
- ? transformCanvasConfig(canvasConfig, vipInfo.isVip)
158
+ ? transformCanvasConfig(canvasConfig, userStatus.isSubscribed)
159
159
  : null;
160
160
  return jsonResult({
161
161
  ok: true,
@@ -163,9 +163,9 @@ export function registerArtfloTools(api, context) {
163
163
  data: {
164
164
  canvasId,
165
165
  subscription: {
166
- isVip: vipInfo.isVip,
167
- membershipLevel: vipInfo.membershipLevel,
168
- membershipName: vipInfo.membershipName,
166
+ plan: userStatus.plan,
167
+ isSubscribed: userStatus.isSubscribed,
168
+ credits: userStatus.credits,
169
169
  },
170
170
  models: llmConfig?.categories ?? [],
171
171
  outputTools: llmConfig?.outputTools ?? [],
@@ -470,7 +470,7 @@ export function registerArtfloTools(api, context) {
470
470
  const session = context.sessions.getOrCreate(payload.canvasId);
471
471
  await session.connect({ canvasId: payload.canvasId });
472
472
  const canvasConfig = session.getCanvasConfig();
473
- const vipInfo = await fetchVipInfo(context.config);
473
+ const userStatus = await fetchUserStatus(context.config);
474
474
  if (!canvasConfig) {
475
475
  return jsonResult({
476
476
  ok: false,
@@ -499,7 +499,7 @@ export function registerArtfloTools(api, context) {
499
499
  const isVideo = VIDEO_TOOL_TYPES.has(info.toolType);
500
500
  const mapped = models.map((m) => {
501
501
  const modelAvailable = !m.disable &&
502
- (!m.subscribe_available || vipInfo.isVip || m.is_free_limit);
502
+ (!m.subscribe_available || userStatus.isSubscribed);
503
503
  // Build per-resolution pricing
504
504
  const resolutionPricing = m.multi_config_price && Object.keys(m.multi_config_price).length > 0
505
505
  ? Object.fromEntries(Object.entries(m.multi_config_price).map(([key, price]) => {
@@ -544,11 +544,12 @@ export function registerArtfloTools(api, context) {
544
544
  label: ARTFLO_TOOL_NAMES.listModels,
545
545
  data: {
546
546
  subscription: {
547
- isVip: vipInfo.isVip,
548
- membershipLevel: vipInfo.membershipLevel,
547
+ plan: userStatus.plan,
548
+ isSubscribed: userStatus.isSubscribed,
549
+ credits: userStatus.credits,
549
550
  pricingUrl: 'https://artflo.ai/pricing',
550
551
  },
551
- hint: vipInfo.isVip
552
+ hint: userStatus.isSubscribed
552
553
  ? undefined
553
554
  : 'User is not subscribed. Models or resolutions marked requiresSubscription need a subscription. Visit https://artflo.ai/pricing to subscribe, or use free models.',
554
555
  categories: result,
@@ -582,6 +583,28 @@ export function registerArtfloTools(api, context) {
582
583
  });
583
584
  },
584
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
+ });
585
608
  api.registerTool({
586
609
  name: ARTFLO_TOOL_NAMES.setApiKey,
587
610
  label: 'Artflo Set Config',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artflo-ai/artflo-openclaw-plugin",
3
- "version": "0.0.5",
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"
@@ -48,6 +48,7 @@ metadata:
48
48
  - 在假设画布运行时已就绪之前,先读取连接状态。
49
49
  - 将插件配置视为 WebSocket 凭证和连接默认值的唯一真实来源。
50
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`。
51
52
  - **资源保存路径**:画布生成完成后需要下载保存的资源(图片、视频等),必须保存到 `/artflo/outbound` 目录。不要使用 `/tmp` 或其他临时目录。
52
53
  - 当插件布局引擎可用时,不要手工编造节点坐标。
53
54
 
@@ -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
- }