@folotoy/folotoy-openclaw-plugin 0.3.0 → 0.4.0
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/bin/folotoy.mjs +2 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.d.ts.map +1 -0
- package/dist/cli/install.js +113 -0
- package/dist/cli/install.js.map +1 -0
- package/package.json +9 -2
- package/src/cli/install.ts +147 -0
- package/src/cli/qrcode-terminal.d.ts +3 -0
- package/src/index.ts +7 -7
package/bin/folotoy.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/cli/install.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import qrcode from 'qrcode-terminal';
|
|
3
|
+
import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT } from '../config.js';
|
|
4
|
+
const PAIR_API_BASE = process.env.PAIR_API_BASE ?? 'https://pair.folotoy.cn';
|
|
5
|
+
const POLL_INTERVAL_MS = 3000;
|
|
6
|
+
const POLL_TIMEOUT_MS = 300_000; // 5 minutes
|
|
7
|
+
// ── Helpers ────────────────────────────────────────────
|
|
8
|
+
function checkOpenClaw() {
|
|
9
|
+
try {
|
|
10
|
+
execSync('openclaw --version', { stdio: 'pipe' });
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
console.error('Error: openclaw is not installed or not in PATH.');
|
|
14
|
+
console.error('Install it first: npm i -g openclaw');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function installPlugin() {
|
|
19
|
+
try {
|
|
20
|
+
const list = execSync('openclaw plugins list', { stdio: 'pipe' }).toString();
|
|
21
|
+
if (list.includes('folotoy-openclaw-plugin')) {
|
|
22
|
+
return; // already installed
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// ignore
|
|
27
|
+
}
|
|
28
|
+
console.log('Installing FoloToy plugin...');
|
|
29
|
+
execSync('openclaw plugins install @folotoy/folotoy-openclaw-plugin', { stdio: 'inherit' });
|
|
30
|
+
}
|
|
31
|
+
async function createSession() {
|
|
32
|
+
const res = await fetch(`${PAIR_API_BASE}/api/pair`, { method: 'POST' });
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
throw new Error(`Failed to create pairing session (HTTP ${res.status})`);
|
|
35
|
+
return res.json();
|
|
36
|
+
}
|
|
37
|
+
function displayQR(url) {
|
|
38
|
+
qrcode.generate(url, { small: true }, (qr) => {
|
|
39
|
+
console.log(qr);
|
|
40
|
+
});
|
|
41
|
+
console.log(`Or open this URL on your phone: ${url}\n`);
|
|
42
|
+
}
|
|
43
|
+
async function sleep(ms) {
|
|
44
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
45
|
+
}
|
|
46
|
+
async function pollSession(sessionId) {
|
|
47
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
48
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
49
|
+
let i = 0;
|
|
50
|
+
while (Date.now() < deadline) {
|
|
51
|
+
process.stdout.write(`\r${frames[i++ % frames.length]} Waiting for pairing...`);
|
|
52
|
+
const res = await fetch(`${PAIR_API_BASE}/api/pair/${sessionId}`);
|
|
53
|
+
if (!res.ok)
|
|
54
|
+
throw new Error(`Poll failed (HTTP ${res.status})`);
|
|
55
|
+
const data = (await res.json());
|
|
56
|
+
if (data.status === 'completed') {
|
|
57
|
+
process.stdout.write('\r\x1b[32m✓\x1b[0m Paired successfully! \n');
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
if (data.status === 'expired') {
|
|
61
|
+
process.stdout.write('\r');
|
|
62
|
+
throw new Error('Pairing session expired. Please try again.');
|
|
63
|
+
}
|
|
64
|
+
await sleep(POLL_INTERVAL_MS);
|
|
65
|
+
}
|
|
66
|
+
throw new Error('Pairing timed out after 5 minutes.');
|
|
67
|
+
}
|
|
68
|
+
function writeConfig(result) {
|
|
69
|
+
execSync(`openclaw config set channels.folotoy.flow direct`, { stdio: 'pipe' });
|
|
70
|
+
execSync(`openclaw config set channels.folotoy.toy_sn ${result.toy_sn}`, { stdio: 'pipe' });
|
|
71
|
+
execSync(`openclaw config set channels.folotoy.toy_key ${result.toy_key}`, { stdio: 'pipe' });
|
|
72
|
+
const mqttHost = result.mqtt_host ?? DEFAULT_MQTT_HOST;
|
|
73
|
+
const mqttPort = result.mqtt_port ?? DEFAULT_MQTT_PORT;
|
|
74
|
+
execSync(`openclaw config set channels.folotoy.mqtt_host ${mqttHost}`, { stdio: 'pipe' });
|
|
75
|
+
execSync(`openclaw config set channels.folotoy.mqtt_port ${mqttPort}`, { stdio: 'pipe' });
|
|
76
|
+
}
|
|
77
|
+
// ── Main ───────────────────────────────────────────────
|
|
78
|
+
async function main() {
|
|
79
|
+
const command = process.argv[2];
|
|
80
|
+
if (command !== 'install') {
|
|
81
|
+
console.log('Usage: npx @folotoy/folotoy-openclaw-plugin install');
|
|
82
|
+
process.exit(command ? 1 : 0);
|
|
83
|
+
}
|
|
84
|
+
console.log('🧸 FoloToy OpenClaw Plugin Installer\n');
|
|
85
|
+
// Step 1: check prerequisites
|
|
86
|
+
console.log('Checking openclaw...');
|
|
87
|
+
checkOpenClaw();
|
|
88
|
+
console.log('✓ openclaw found\n');
|
|
89
|
+
// Step 2: install plugin if not present
|
|
90
|
+
installPlugin();
|
|
91
|
+
// Step 3: create pairing session
|
|
92
|
+
console.log('Creating pairing session...\n');
|
|
93
|
+
const session = await createSession();
|
|
94
|
+
// Step 4: display QR code
|
|
95
|
+
console.log('Scan this QR code with your phone,');
|
|
96
|
+
console.log('then scan your toy\'s QR code on the phone:\n');
|
|
97
|
+
displayQR(session.pair_url);
|
|
98
|
+
// Step 5: poll for result
|
|
99
|
+
const result = await pollSession(session.session_id);
|
|
100
|
+
// Step 6: write config
|
|
101
|
+
console.log('\nWriting configuration...');
|
|
102
|
+
writeConfig(result);
|
|
103
|
+
// Step 7: done
|
|
104
|
+
console.log('\n\x1b[32m✓ FoloToy plugin installed and configured!\x1b[0m');
|
|
105
|
+
console.log(` Toy SN: ${result.toy_sn}`);
|
|
106
|
+
console.log(` MQTT Host: ${result.mqtt_host ?? DEFAULT_MQTT_HOST}`);
|
|
107
|
+
console.log('\nRestart the gateway to apply: openclaw gateway start --force');
|
|
108
|
+
}
|
|
109
|
+
main().catch((err) => {
|
|
110
|
+
console.error(`\n\x1b[31mError:\x1b[0m ${err instanceof Error ? err.message : String(err)}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/cli/install.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@folotoy/folotoy-openclaw-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Empower your FoloToy with OpenClaw AI capabilities.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"folotoy",
|
|
@@ -22,12 +22,18 @@
|
|
|
22
22
|
},
|
|
23
23
|
"main": "./src/index.ts",
|
|
24
24
|
"types": "./src/index.ts",
|
|
25
|
+
"bin": {
|
|
26
|
+
"folotoy-openclaw-plugin": "./bin/folotoy.mjs"
|
|
27
|
+
},
|
|
25
28
|
"files": [
|
|
26
29
|
"src",
|
|
30
|
+
"bin",
|
|
31
|
+
"dist/cli",
|
|
27
32
|
"openclaw.plugin.json"
|
|
28
33
|
],
|
|
29
34
|
"scripts": {
|
|
30
35
|
"build": "tsc",
|
|
36
|
+
"prepublishOnly": "npm run build",
|
|
31
37
|
"dev": "tsc --watch",
|
|
32
38
|
"typecheck": "tsc --noEmit",
|
|
33
39
|
"test": "vitest run",
|
|
@@ -35,7 +41,8 @@
|
|
|
35
41
|
"clean": "rm -rf dist"
|
|
36
42
|
},
|
|
37
43
|
"dependencies": {
|
|
38
|
-
"mqtt": "^5.10.1"
|
|
44
|
+
"mqtt": "^5.10.1",
|
|
45
|
+
"qrcode-terminal": "^0.12.0"
|
|
39
46
|
},
|
|
40
47
|
"devDependencies": {
|
|
41
48
|
"@types/node": "^22.13.10",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import qrcode from 'qrcode-terminal'
|
|
3
|
+
import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT } from '../config.js'
|
|
4
|
+
|
|
5
|
+
const PAIR_API_BASE = process.env.PAIR_API_BASE ?? 'https://pair.folotoy.cn'
|
|
6
|
+
const POLL_INTERVAL_MS = 3000
|
|
7
|
+
const POLL_TIMEOUT_MS = 300_000 // 5 minutes
|
|
8
|
+
|
|
9
|
+
// ── Types ──────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
type CreateSessionResponse = {
|
|
12
|
+
session_id: string
|
|
13
|
+
pair_url: string
|
|
14
|
+
expires_at: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type PollResponse =
|
|
18
|
+
| { status: 'pending' }
|
|
19
|
+
| { status: 'completed'; toy_sn: string; toy_key: string; mqtt_host?: string; mqtt_port?: number }
|
|
20
|
+
| { status: 'expired' }
|
|
21
|
+
|
|
22
|
+
// ── Helpers ────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function checkOpenClaw(): void {
|
|
25
|
+
try {
|
|
26
|
+
execSync('openclaw --version', { stdio: 'pipe' })
|
|
27
|
+
} catch {
|
|
28
|
+
console.error('Error: openclaw is not installed or not in PATH.')
|
|
29
|
+
console.error('Install it first: npm i -g openclaw')
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function installPlugin(): void {
|
|
35
|
+
try {
|
|
36
|
+
const list = execSync('openclaw plugins list', { stdio: 'pipe' }).toString()
|
|
37
|
+
if (list.includes('folotoy-openclaw-plugin')) {
|
|
38
|
+
return // already installed
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// ignore
|
|
42
|
+
}
|
|
43
|
+
console.log('Installing FoloToy plugin...')
|
|
44
|
+
execSync('openclaw plugins install @folotoy/folotoy-openclaw-plugin', { stdio: 'inherit' })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function createSession(): Promise<CreateSessionResponse> {
|
|
48
|
+
const res = await fetch(`${PAIR_API_BASE}/api/pair`, { method: 'POST' })
|
|
49
|
+
if (!res.ok) throw new Error(`Failed to create pairing session (HTTP ${res.status})`)
|
|
50
|
+
return res.json() as Promise<CreateSessionResponse>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function displayQR(url: string): void {
|
|
54
|
+
qrcode.generate(url, { small: true }, (qr: string) => {
|
|
55
|
+
console.log(qr)
|
|
56
|
+
})
|
|
57
|
+
console.log(`Or open this URL on your phone: ${url}\n`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function sleep(ms: number): Promise<void> {
|
|
61
|
+
return new Promise((r) => setTimeout(r, ms))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function pollSession(sessionId: string): Promise<PollResponse & { status: 'completed' }> {
|
|
65
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS
|
|
66
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
67
|
+
let i = 0
|
|
68
|
+
|
|
69
|
+
while (Date.now() < deadline) {
|
|
70
|
+
process.stdout.write(`\r${frames[i++ % frames.length]} Waiting for pairing...`)
|
|
71
|
+
|
|
72
|
+
const res = await fetch(`${PAIR_API_BASE}/api/pair/${sessionId}`)
|
|
73
|
+
if (!res.ok) throw new Error(`Poll failed (HTTP ${res.status})`)
|
|
74
|
+
const data = (await res.json()) as PollResponse
|
|
75
|
+
|
|
76
|
+
if (data.status === 'completed') {
|
|
77
|
+
process.stdout.write('\r\x1b[32m✓\x1b[0m Paired successfully! \n')
|
|
78
|
+
return data as PollResponse & { status: 'completed' }
|
|
79
|
+
}
|
|
80
|
+
if (data.status === 'expired') {
|
|
81
|
+
process.stdout.write('\r')
|
|
82
|
+
throw new Error('Pairing session expired. Please try again.')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await sleep(POLL_INTERVAL_MS)
|
|
86
|
+
}
|
|
87
|
+
throw new Error('Pairing timed out after 5 minutes.')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeConfig(result: { toy_sn: string; toy_key: string; mqtt_host?: string; mqtt_port?: number }): void {
|
|
91
|
+
execSync(`openclaw config set channels.folotoy.flow direct`, { stdio: 'pipe' })
|
|
92
|
+
execSync(`openclaw config set channels.folotoy.toy_sn ${result.toy_sn}`, { stdio: 'pipe' })
|
|
93
|
+
execSync(`openclaw config set channels.folotoy.toy_key ${result.toy_key}`, { stdio: 'pipe' })
|
|
94
|
+
|
|
95
|
+
const mqttHost = result.mqtt_host ?? DEFAULT_MQTT_HOST
|
|
96
|
+
const mqttPort = result.mqtt_port ?? DEFAULT_MQTT_PORT
|
|
97
|
+
execSync(`openclaw config set channels.folotoy.mqtt_host ${mqttHost}`, { stdio: 'pipe' })
|
|
98
|
+
execSync(`openclaw config set channels.folotoy.mqtt_port ${mqttPort}`, { stdio: 'pipe' })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Main ───────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
async function main() {
|
|
104
|
+
const command = process.argv[2]
|
|
105
|
+
|
|
106
|
+
if (command !== 'install') {
|
|
107
|
+
console.log('Usage: npx @folotoy/folotoy-openclaw-plugin install')
|
|
108
|
+
process.exit(command ? 1 : 0)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('🧸 FoloToy OpenClaw Plugin Installer\n')
|
|
112
|
+
|
|
113
|
+
// Step 1: check prerequisites
|
|
114
|
+
console.log('Checking openclaw...')
|
|
115
|
+
checkOpenClaw()
|
|
116
|
+
console.log('✓ openclaw found\n')
|
|
117
|
+
|
|
118
|
+
// Step 2: install plugin if not present
|
|
119
|
+
installPlugin()
|
|
120
|
+
|
|
121
|
+
// Step 3: create pairing session
|
|
122
|
+
console.log('Creating pairing session...\n')
|
|
123
|
+
const session = await createSession()
|
|
124
|
+
|
|
125
|
+
// Step 4: display QR code
|
|
126
|
+
console.log('Scan this QR code with your phone,')
|
|
127
|
+
console.log('then scan your toy\'s QR code on the phone:\n')
|
|
128
|
+
displayQR(session.pair_url)
|
|
129
|
+
|
|
130
|
+
// Step 5: poll for result
|
|
131
|
+
const result = await pollSession(session.session_id)
|
|
132
|
+
|
|
133
|
+
// Step 6: write config
|
|
134
|
+
console.log('\nWriting configuration...')
|
|
135
|
+
writeConfig(result)
|
|
136
|
+
|
|
137
|
+
// Step 7: done
|
|
138
|
+
console.log('\n\x1b[32m✓ FoloToy plugin installed and configured!\x1b[0m')
|
|
139
|
+
console.log(` Toy SN: ${result.toy_sn}`)
|
|
140
|
+
console.log(` MQTT Host: ${result.mqtt_host ?? DEFAULT_MQTT_HOST}`)
|
|
141
|
+
console.log('\nRestart the gateway to apply: openclaw gateway start --force')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
main().catch((err) => {
|
|
145
|
+
console.error(`\n\x1b[31mError:\x1b[0m ${err instanceof Error ? err.message : String(err)}`)
|
|
146
|
+
process.exit(1)
|
|
147
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -134,16 +134,16 @@ const folotoyChannel: ChannelPlugin<FlatChannelConfig> = {
|
|
|
134
134
|
if (msg.identifier !== 'chat_input' || typeof msg.inputParams?.text !== 'string') return
|
|
135
135
|
|
|
136
136
|
const { msgId, inputParams: { text, recording_id } } = msg
|
|
137
|
-
let order =
|
|
137
|
+
let order = 1
|
|
138
138
|
|
|
139
|
-
// Send a quick soothing acknowledgment before AI processing
|
|
140
|
-
|
|
141
|
-
const
|
|
139
|
+
// Send a quick soothing acknowledgment before AI processing (order=1).
|
|
140
|
+
// AI replies continue from order=2.
|
|
141
|
+
const ackMsg: OutboundMessage = {
|
|
142
142
|
msgId,
|
|
143
|
-
identifier: '
|
|
144
|
-
outParams: {
|
|
143
|
+
identifier: 'chat_output',
|
|
144
|
+
outParams: { content: pickSoothingReply(text), recording_id, order, is_finished: false },
|
|
145
145
|
}
|
|
146
|
-
client.publish(
|
|
146
|
+
client.publish(outboundTopic, JSON.stringify(ackMsg))
|
|
147
147
|
|
|
148
148
|
const inboundCtx = channelRuntime.reply.finalizeInboundContext({
|
|
149
149
|
Body: text,
|