@artflo-ai/artflo-openclaw-plugin 0.0.1
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 +102 -0
- package/dist/index.js +73 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-00-216Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-00-217Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-05-727Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-30-218Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-30-218Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-35-728Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-00-218Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-00-219Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-05-729Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-30-220Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-30-220Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-35-729Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-00-221Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-00-221Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-05-730Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-30-222Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-30-222Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-35-731Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-00-223Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-00-223Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-05-732Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-30-223Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-30-223Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-35-734Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-34-00-228Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-34-00-229Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/src/config.js +57 -0
- package/dist/src/constants.js +35 -0
- package/dist/src/core/api/api-base.js +12 -0
- package/dist/src/core/api/upload-file.js +59 -0
- package/dist/src/core/canvas/canvas-session-manager.js +189 -0
- package/dist/src/core/canvas/canvas-websocket-client.js +453 -0
- package/dist/src/core/canvas/create-canvas.js +37 -0
- package/dist/src/core/canvas/types.js +23 -0
- package/dist/src/core/canvas/ws-trace.js +42 -0
- package/dist/src/core/config/fetch-client-params.js +20 -0
- package/dist/src/core/config/fetch-vip-info.js +30 -0
- package/dist/src/core/config/model-config-transformer.js +104 -0
- package/dist/src/core/executor/element-builders.js +216 -0
- package/dist/src/core/executor/execute-plan.js +1221 -0
- package/dist/src/core/executor/execution-trace.js +34 -0
- package/dist/src/core/layout/layout-service.js +366 -0
- package/dist/src/core/plan/analyze-plan-groups.js +71 -0
- package/dist/src/core/plan/types.js +1 -0
- package/dist/src/core/plan/validate-plan.js +159 -0
- package/dist/src/paths.js +16 -0
- package/dist/src/services/canvas-session-registry.js +57 -0
- package/dist/src/tools/register-tools.js +669 -0
- package/dist/src/tools/tool-trace.js +19 -0
- package/openclaw.plugin.json +33 -0
- package/package.json +42 -0
- package/skills/artflo-canvas/SKILL.md +118 -0
- package/skills/artflo-canvas/references/graph-rules.md +53 -0
- package/skills/artflo-canvas/references/layout-notes.md +31 -0
- package/skills/artflo-canvas/references/node-schema.json +948 -0
- package/skills/artflo-canvas/references/node-schema.md +188 -0
- package/skills/artflo-canvas/references/planning-guide.md +321 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Artflo OpenClaw Plugin
|
|
2
|
+
|
|
3
|
+
OpenClaw plugin for Artflo canvas operations over direct WebSocket.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw plugins install @artflo-ai/artflo-openclaw-plugin
|
|
11
|
+
openclaw plugins enable artflo-openclaw-plugin
|
|
12
|
+
openclaw gateway restart
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Configure
|
|
16
|
+
|
|
17
|
+
Only one required parameter: `apiKey`.
|
|
18
|
+
|
|
19
|
+
The easiest way: just start chatting. The plugin will detect missing API Key
|
|
20
|
+
and guide you through setup in the conversation.
|
|
21
|
+
|
|
22
|
+
Or manually edit `~/.openclaw/openclaw.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"artflo-openclaw-plugin": {
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"config": {
|
|
29
|
+
"apiKey": "YOUR_API_KEY"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Get your API Key from [artflo.ai](https://artflo.ai) settings page.
|
|
36
|
+
|
|
37
|
+
### Switch Environment
|
|
38
|
+
|
|
39
|
+
Default is `release` (production). For test environment:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"config": {
|
|
44
|
+
"env": "test",
|
|
45
|
+
"apiKey": "YOUR_API_KEY"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
| Parameter | Required | Default | Description |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| `apiKey` | Yes | — | Artflo API Key |
|
|
55
|
+
| `env` | No | `release` | `release` (production) or `test` |
|
|
56
|
+
| `operSystem` | No | `7` | Client OS identifier |
|
|
57
|
+
|
|
58
|
+
Other parameters (`appKey`, `deviceId`, `timeZone`, `countryCode`, etc.)
|
|
59
|
+
are auto-detected or fetched from the Artflo API at startup.
|
|
60
|
+
|
|
61
|
+
## Tools
|
|
62
|
+
|
|
63
|
+
- `artflo_canvas_connect` — Connect to canvas WebSocket
|
|
64
|
+
- `artflo_canvas_connection_status` — Check connection status
|
|
65
|
+
- `artflo_canvas_get_state` — Get canvas state
|
|
66
|
+
- `artflo_canvas_get_config` — Get available models and config
|
|
67
|
+
- `artflo_canvas_get_node` — Read a single node
|
|
68
|
+
- `artflo_canvas_find_nodes` — Search nodes
|
|
69
|
+
- `artflo_canvas_change_elements` — Modify nodes
|
|
70
|
+
- `artflo_canvas_delete_elements` — Delete nodes
|
|
71
|
+
- `artflo_canvas_run_nodes` — Execute nodes
|
|
72
|
+
- `artflo_canvas_wait_for_completion` — Wait for node completion
|
|
73
|
+
- `artflo_canvas_execute_plan` — Execute a structured workflow plan
|
|
74
|
+
- `artflo_canvas_create` — Create a new canvas
|
|
75
|
+
- `artflo_canvas_list_models` — List available models with pricing
|
|
76
|
+
- `artflo_upload_file` — Upload files to Artflo storage
|
|
77
|
+
- `artflo_set_api_key` — Save API Key to config (used in chat onboarding)
|
|
78
|
+
|
|
79
|
+
## Local Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git clone <repo-url>
|
|
83
|
+
cd artflo-openclaw-ws-plugin
|
|
84
|
+
pnpm install
|
|
85
|
+
pnpm check # type check only
|
|
86
|
+
pnpm build # compile to dist/
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Link for local testing
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
openclaw plugins install -l .
|
|
93
|
+
openclaw plugins enable artflo-openclaw-plugin
|
|
94
|
+
openclaw gateway restart
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Publish
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pnpm build
|
|
101
|
+
npm publish
|
|
102
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createPluginConfig } from './src/config.js';
|
|
2
|
+
import { createSessionRegistryService } from './src/services/canvas-session-registry.js';
|
|
3
|
+
import { registerArtfloTools } from './src/tools/register-tools.js';
|
|
4
|
+
import { fetchClientParams } from './src/core/config/fetch-client-params.js';
|
|
5
|
+
import { PLUGIN_DESCRIPTION, PLUGIN_ID, PLUGIN_NAME } from './src/constants.js';
|
|
6
|
+
const pluginConfigSchema = {
|
|
7
|
+
type: 'object',
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
properties: {
|
|
10
|
+
env: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
enum: ['release', 'test'],
|
|
13
|
+
default: 'release',
|
|
14
|
+
description: 'Environment: release (production) or test.',
|
|
15
|
+
},
|
|
16
|
+
apiKey: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Artflo API Key for authentication. Get it from https://artflo.ai/settings.',
|
|
19
|
+
},
|
|
20
|
+
operSystem: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
default: '7',
|
|
23
|
+
description: 'Client oper_system identifier for WebSocket connections.',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const plugin = {
|
|
28
|
+
id: PLUGIN_ID,
|
|
29
|
+
name: PLUGIN_NAME,
|
|
30
|
+
description: PLUGIN_DESCRIPTION,
|
|
31
|
+
configSchema: pluginConfigSchema,
|
|
32
|
+
async register(api) {
|
|
33
|
+
const config = createPluginConfig(api);
|
|
34
|
+
// ── 没有 apiKey 时跳过所有初始化,只注册 tool(tool 内部会检查 apiKey)──
|
|
35
|
+
if (!config.apiKey) {
|
|
36
|
+
console.log(`[artflo] API Key not configured. Skipping initialization.`);
|
|
37
|
+
const sessions = createSessionRegistryService(config);
|
|
38
|
+
api.registerService(sessions.service);
|
|
39
|
+
registerArtfloTools(api, { config, sessions: sessions.registry });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// ── 自动获取 appKey / vipAppId / vipGroup ─────────────────────────
|
|
43
|
+
if (!config.appKey || !config.vipAppId || !config.vipGroup) {
|
|
44
|
+
try {
|
|
45
|
+
const params = await fetchClientParams(config.webApiBaseUrl);
|
|
46
|
+
if (!config.appKey)
|
|
47
|
+
config.appKey = params.appKey;
|
|
48
|
+
if (!config.vipAppId)
|
|
49
|
+
config.vipAppId = params.vipAppId;
|
|
50
|
+
if (!config.vipGroup)
|
|
51
|
+
config.vipGroup = params.vipGroup;
|
|
52
|
+
console.log(`[artflo] client params fetched: appKey=${config.appKey}, env=${config.env}`);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.warn(`[artflo] Failed to fetch client params, using defaults:`, err instanceof Error ? err.message : String(err));
|
|
56
|
+
if (!config.appKey)
|
|
57
|
+
config.appKey = 'A40CB4D42E28F808';
|
|
58
|
+
if (!config.vipAppId)
|
|
59
|
+
config.vipAppId = '7029803307044000000';
|
|
60
|
+
if (!config.vipGroup)
|
|
61
|
+
config.vipGroup = 'group_artflo';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
console.log(`[artflo] env=${config.env}, api=${config.apiBaseUrl}`);
|
|
65
|
+
const sessions = createSessionRegistryService(config);
|
|
66
|
+
api.registerService(sessions.service);
|
|
67
|
+
registerArtfloTools(api, {
|
|
68
|
+
config,
|
|
69
|
+
sessions: sessions.registry,
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
export default plugin;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { ENV_CONFIG, DEFAULT_MAX_RECONNECT_ATTEMPTS, DEFAULT_OPER_SYSTEM, DEFAULT_REQUEST_TIMEOUT_MS, } from './constants.js';
|
|
4
|
+
function readString(value, fallback) {
|
|
5
|
+
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
6
|
+
}
|
|
7
|
+
function readNumber(value, fallback) {
|
|
8
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
9
|
+
}
|
|
10
|
+
function buildStableDefaultDeviceId() {
|
|
11
|
+
const fingerprint = createHash('sha1')
|
|
12
|
+
.update(`${os.hostname()}:${os.userInfo().username}`)
|
|
13
|
+
.digest('hex')
|
|
14
|
+
.slice(0, 8);
|
|
15
|
+
return `artflo-openclaw-${fingerprint}`;
|
|
16
|
+
}
|
|
17
|
+
function detectTimeZone() {
|
|
18
|
+
try {
|
|
19
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Shanghai';
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return 'Asia/Shanghai';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function detectCountryCode() {
|
|
26
|
+
try {
|
|
27
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale || '';
|
|
28
|
+
const parts = locale.split('-');
|
|
29
|
+
return parts.length >= 2 ? parts[parts.length - 1].toUpperCase() : 'CN';
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return 'CN';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function createPluginConfig(api) {
|
|
36
|
+
const pluginConfig = api.pluginConfig ?? {};
|
|
37
|
+
const env = (typeof pluginConfig.env === 'string' && (pluginConfig.env === 'release' || pluginConfig.env === 'test')
|
|
38
|
+
? pluginConfig.env
|
|
39
|
+
: 'release');
|
|
40
|
+
const urls = ENV_CONFIG[env];
|
|
41
|
+
return {
|
|
42
|
+
env,
|
|
43
|
+
apiBaseUrl: urls.apiBaseUrl,
|
|
44
|
+
vipApiBaseUrl: urls.vipApiBaseUrl,
|
|
45
|
+
webApiBaseUrl: urls.webApiBaseUrl,
|
|
46
|
+
apiKey: readString(pluginConfig.apiKey, ''),
|
|
47
|
+
appKey: readString(pluginConfig.appKey, ''),
|
|
48
|
+
vipAppId: readString(pluginConfig.vipAppId, ''),
|
|
49
|
+
vipGroup: readString(pluginConfig.vipGroup, ''),
|
|
50
|
+
deviceId: readString(pluginConfig.deviceId, buildStableDefaultDeviceId()),
|
|
51
|
+
timeZone: readString(pluginConfig.timeZone, detectTimeZone()),
|
|
52
|
+
countryCode: readString(pluginConfig.countryCode, detectCountryCode()),
|
|
53
|
+
operSystem: readString(pluginConfig.operSystem, DEFAULT_OPER_SYSTEM),
|
|
54
|
+
requestTimeoutMs: readNumber(pluginConfig.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS),
|
|
55
|
+
maxReconnectAttempts: readNumber(pluginConfig.maxReconnectAttempts, DEFAULT_MAX_RECONNECT_ATTEMPTS),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const PLUGIN_ID = 'artflo-openclaw-plugin';
|
|
2
|
+
export const PLUGIN_NAME = 'Artflo OpenClaw Plugin';
|
|
3
|
+
export const PLUGIN_DESCRIPTION = 'Expose deterministic Artflo canvas tools over direct WebSocket execution.';
|
|
4
|
+
export const ENV_CONFIG = {
|
|
5
|
+
release: {
|
|
6
|
+
apiBaseUrl: 'https://webapi.artflo.ai',
|
|
7
|
+
vipApiBaseUrl: 'https://api-h5-sub.starii.com',
|
|
8
|
+
webApiBaseUrl: 'https://artflo.ai',
|
|
9
|
+
},
|
|
10
|
+
test: {
|
|
11
|
+
apiBaseUrl: 'https://testwebapi.artflo.ai',
|
|
12
|
+
vipApiBaseUrl: 'https://pre-api-h5-sub.starii.com',
|
|
13
|
+
webApiBaseUrl: 'https://test.artflo.ai',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
export const DEFAULT_OPER_SYSTEM = '7';
|
|
17
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
|
|
18
|
+
export const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
|
|
19
|
+
export const ARTFLO_TOOL_NAMES = {
|
|
20
|
+
connect: 'artflo_canvas_connect',
|
|
21
|
+
connectionStatus: 'artflo_canvas_connection_status',
|
|
22
|
+
getState: 'artflo_canvas_get_state',
|
|
23
|
+
getConfig: 'artflo_canvas_get_config',
|
|
24
|
+
findNodes: 'artflo_canvas_find_nodes',
|
|
25
|
+
getNode: 'artflo_canvas_get_node',
|
|
26
|
+
changeElements: 'artflo_canvas_change_elements',
|
|
27
|
+
deleteElements: 'artflo_canvas_delete_elements',
|
|
28
|
+
runNodes: 'artflo_canvas_run_nodes',
|
|
29
|
+
waitForCompletion: 'artflo_canvas_wait_for_completion',
|
|
30
|
+
executePlan: 'artflo_canvas_execute_plan',
|
|
31
|
+
createCanvas: 'artflo_canvas_create',
|
|
32
|
+
listModels: 'artflo_canvas_list_models',
|
|
33
|
+
uploadFile: 'artflo_upload_file',
|
|
34
|
+
setApiKey: 'artflo_set_api_key',
|
|
35
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload a file to Artflo OBS storage.
|
|
3
|
+
*
|
|
4
|
+
* POST /storage/obs/starii (multipart/form-data, field "file")
|
|
5
|
+
* Auth: X-API-Key header.
|
|
6
|
+
* Max 100 MiB; common image/video extensions.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { basename } from 'node:path';
|
|
10
|
+
import { getApiBaseUrl } from './api-base.js';
|
|
11
|
+
/**
|
|
12
|
+
* Upload a local file or a remote URL to Artflo OBS.
|
|
13
|
+
*
|
|
14
|
+
* @param source - Local file path or remote https URL.
|
|
15
|
+
* @param config - Plugin config (provides access token and WS URL for API base).
|
|
16
|
+
* @param filename - Optional override for the uploaded filename.
|
|
17
|
+
*/
|
|
18
|
+
export async function uploadFile(config, source, filename) {
|
|
19
|
+
let fileBlob;
|
|
20
|
+
let resolvedFilename;
|
|
21
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
22
|
+
// Remote URL — download first
|
|
23
|
+
const resp = await fetch(source);
|
|
24
|
+
if (!resp.ok) {
|
|
25
|
+
throw new Error(`Failed to download ${source}: HTTP ${resp.status}`);
|
|
26
|
+
}
|
|
27
|
+
const buffer = await resp.arrayBuffer();
|
|
28
|
+
const contentType = resp.headers.get('content-type') || 'application/octet-stream';
|
|
29
|
+
fileBlob = new Blob([buffer], { type: contentType });
|
|
30
|
+
resolvedFilename = filename || basename(new URL(source).pathname) || 'upload';
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// Local file path
|
|
34
|
+
const buffer = await readFile(source);
|
|
35
|
+
fileBlob = new Blob([buffer]);
|
|
36
|
+
resolvedFilename = filename || basename(source);
|
|
37
|
+
}
|
|
38
|
+
const formData = new FormData();
|
|
39
|
+
formData.append('file', fileBlob, resolvedFilename);
|
|
40
|
+
const baseUrl = getApiBaseUrl(config);
|
|
41
|
+
const response = await fetch(`${baseUrl}/storage/obs/starii`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'X-API-Key': config.apiKey,
|
|
45
|
+
},
|
|
46
|
+
body: formData,
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`Upload failed: HTTP ${response.status}`);
|
|
50
|
+
}
|
|
51
|
+
const body = (await response.json());
|
|
52
|
+
if (body.code !== 0 || !body.data?.url) {
|
|
53
|
+
throw new Error(`Upload failed: code=${body.code}`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
url: body.data.url,
|
|
57
|
+
key: body.data.key ?? '',
|
|
58
|
+
};
|
|
59
|
+
}
|