@kln-mcp/ctrl-mobile-cn 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/bin/install.js +314 -0
- package/cli/setup.js +304 -0
- package/config/index.d.ts +19 -0
- package/config/index.js +78 -0
- package/config/pro.json +12 -0
- package/core/index.d.ts +57 -0
- package/core/index.js +315 -0
- package/index.js +23 -0
- package/package.json +31 -0
- package/skills/kln-mobile-ctrl/SKILL.md +122 -0
- package/wrapper/index.d.ts +107 -0
- package/wrapper/index.js +2553 -0
- package/wrapper/live-view.html +438 -0
package/config/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const CONFIG_FILES = {
|
|
7
|
+
dev: 'dev.json',
|
|
8
|
+
pro: 'pro.json'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function normalizeEnv(value) {
|
|
12
|
+
const env = String(value || 'dev').trim().toLowerCase();
|
|
13
|
+
if (env === 'dev' || env === 'development') {
|
|
14
|
+
return 'dev';
|
|
15
|
+
}
|
|
16
|
+
if (env === 'pro' || env === 'prod' || env === 'production') {
|
|
17
|
+
return 'pro';
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Invalid config environment "${env}". Expected dev or pro.`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveConfigPath(runtimeEnv) {
|
|
23
|
+
const env = normalizeEnv(runtimeEnv || process.env.KLN_ENV || process.env.NODE_ENV);
|
|
24
|
+
return path.join(__dirname, CONFIG_FILES[env]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadConfig(runtimeEnv) {
|
|
28
|
+
const file = resolveConfigPath(runtimeEnv);
|
|
29
|
+
const config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
30
|
+
|
|
31
|
+
validateConfig(config, file);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
env: normalizeEnv(runtimeEnv || process.env.KLN_ENV || process.env.NODE_ENV),
|
|
35
|
+
api_url: config.api_url,
|
|
36
|
+
relay_urls: config.relay_urls,
|
|
37
|
+
openobserve: config.openobserve
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validateConfig(config, file) {
|
|
42
|
+
if (typeof config.api_url !== 'string' || config.api_url.trim() === '') {
|
|
43
|
+
throw new Error(`Invalid config ${file}: api_url must be a non-empty string`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!Array.isArray(config.relay_urls) || config.relay_urls.length === 0) {
|
|
47
|
+
throw new Error(`Invalid config ${file}: relay_urls must be a non-empty string array`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const relayUrl of config.relay_urls) {
|
|
51
|
+
if (typeof relayUrl !== 'string' || relayUrl.trim() === '') {
|
|
52
|
+
throw new Error(`Invalid config ${file}: relay_urls must contain only non-empty strings`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
validateOpenObserve(config.openobserve, file);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateOpenObserve(openobserve, file) {
|
|
60
|
+
if (!openobserve || typeof openobserve !== 'object') {
|
|
61
|
+
throw new Error(`Invalid config ${file}: openobserve must be an object`);
|
|
62
|
+
}
|
|
63
|
+
if (typeof openobserve.enabled !== 'boolean') {
|
|
64
|
+
throw new Error(`Invalid config ${file}: openobserve.enabled must be a boolean`);
|
|
65
|
+
}
|
|
66
|
+
if (typeof openobserve.upload_url !== 'string' || openobserve.upload_url.trim() === '') {
|
|
67
|
+
throw new Error(`Invalid config ${file}: openobserve.upload_url must be a non-empty string`);
|
|
68
|
+
}
|
|
69
|
+
if (typeof openobserve.service_name !== 'string' || openobserve.service_name.trim() === '') {
|
|
70
|
+
throw new Error(`Invalid config ${file}: openobserve.service_name must be a non-empty string`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
loadConfig,
|
|
76
|
+
normalizeEnv,
|
|
77
|
+
resolveConfigPath
|
|
78
|
+
};
|
package/config/pro.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"api_url": "http://192.168.3.33:30001",
|
|
3
|
+
"relay_urls": ["http://192.168.3.33:80"],
|
|
4
|
+
"openobserve": {
|
|
5
|
+
"enabled": true,
|
|
6
|
+
"upload_url": "http://192.168.3.33:30001/api/default/kln_mcp/_json",
|
|
7
|
+
"service_name": "mcp",
|
|
8
|
+
"batch_max_records": 500,
|
|
9
|
+
"batch_flush_ms": 5000,
|
|
10
|
+
"disk_buffer_dir": "./runtime/openobserve-buffer/mcp"
|
|
11
|
+
}
|
|
12
|
+
}
|
package/core/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/* auto-generated by NAPI-RS */
|
|
5
|
+
|
|
6
|
+
export interface JsH264Packet {
|
|
7
|
+
kind: number
|
|
8
|
+
flags: number
|
|
9
|
+
width: number
|
|
10
|
+
height: number
|
|
11
|
+
presentationTimeUs: number
|
|
12
|
+
capturedAtMs: number
|
|
13
|
+
data: Buffer
|
|
14
|
+
endReason?: string
|
|
15
|
+
}
|
|
16
|
+
export interface JsH264StreamStats {
|
|
17
|
+
packets: number
|
|
18
|
+
bytes: number
|
|
19
|
+
endReason: string
|
|
20
|
+
}
|
|
21
|
+
export interface JsJsonRpcMessage {
|
|
22
|
+
ok: boolean
|
|
23
|
+
id?: any
|
|
24
|
+
result?: any
|
|
25
|
+
error?: any
|
|
26
|
+
}
|
|
27
|
+
export interface JsAgentMessage {
|
|
28
|
+
version: number
|
|
29
|
+
messageId: string
|
|
30
|
+
replyTo?: string
|
|
31
|
+
unixTime: number
|
|
32
|
+
senderId: string
|
|
33
|
+
kind: string
|
|
34
|
+
body: any
|
|
35
|
+
summary: string
|
|
36
|
+
}
|
|
37
|
+
export interface ConnectOptions {
|
|
38
|
+
ticket: string
|
|
39
|
+
relayUrls?: Array<string>
|
|
40
|
+
alpn?: string
|
|
41
|
+
apiKey?: string
|
|
42
|
+
}
|
|
43
|
+
export interface SendOptions {
|
|
44
|
+
timeoutMs?: number
|
|
45
|
+
traceId?: string
|
|
46
|
+
}
|
|
47
|
+
export declare class KlnCoreClient {
|
|
48
|
+
static connect(options: ConnectOptions): Promise<KlnCoreClient>
|
|
49
|
+
get nodeId(): string
|
|
50
|
+
sendCommand(name: string, bodyText?: string | undefined | null, options?: SendOptions | undefined | null): Promise<JsAgentMessage>
|
|
51
|
+
sendText(bodyText: string, options?: SendOptions | undefined | null): Promise<JsAgentMessage>
|
|
52
|
+
sendOnlyCommand(name: string, bodyText?: string | undefined | null, options?: SendOptions | undefined | null): Promise<string>
|
|
53
|
+
sendJsonRpc(method: string, params?: any | undefined | null, options?: SendOptions | undefined | null): Promise<JsJsonRpcMessage>
|
|
54
|
+
startH264Stream(bodyText: string | undefined | null, outputDir: string, options?: SendOptions | undefined | null): Promise<string>
|
|
55
|
+
startH264Live(bodyText: string | undefined | null, onPacket: (...args: any[]) => any, options?: SendOptions | undefined | null): void
|
|
56
|
+
close(): Promise<void>
|
|
57
|
+
}
|
package/core/index.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
/* prettier-ignore */
|
|
4
|
+
|
|
5
|
+
/* auto-generated by NAPI-RS */
|
|
6
|
+
|
|
7
|
+
const { existsSync, readFileSync } = require('fs')
|
|
8
|
+
const { join } = require('path')
|
|
9
|
+
|
|
10
|
+
const { platform, arch } = process
|
|
11
|
+
|
|
12
|
+
let nativeBinding = null
|
|
13
|
+
let localFileExisted = false
|
|
14
|
+
let loadError = null
|
|
15
|
+
|
|
16
|
+
function isMusl() {
|
|
17
|
+
// For Node 10
|
|
18
|
+
if (!process.report || typeof process.report.getReport !== 'function') {
|
|
19
|
+
try {
|
|
20
|
+
const lddPath = require('child_process').execSync('which ldd').toString().trim()
|
|
21
|
+
return readFileSync(lddPath, 'utf8').includes('musl')
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
const { glibcVersionRuntime } = process.report.getReport().header
|
|
27
|
+
return !glibcVersionRuntime
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
switch (platform) {
|
|
32
|
+
case 'android':
|
|
33
|
+
switch (arch) {
|
|
34
|
+
case 'arm64':
|
|
35
|
+
localFileExisted = existsSync(join(__dirname, 'kln_ctrl_core.android-arm64.node'))
|
|
36
|
+
try {
|
|
37
|
+
if (localFileExisted) {
|
|
38
|
+
nativeBinding = require('./kln_ctrl_core.android-arm64.node')
|
|
39
|
+
} else {
|
|
40
|
+
nativeBinding = require('@kln-ctrl/mobile-core-android-arm64')
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
loadError = e
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
case 'arm':
|
|
47
|
+
localFileExisted = existsSync(join(__dirname, 'kln_ctrl_core.android-arm-eabi.node'))
|
|
48
|
+
try {
|
|
49
|
+
if (localFileExisted) {
|
|
50
|
+
nativeBinding = require('./kln_ctrl_core.android-arm-eabi.node')
|
|
51
|
+
} else {
|
|
52
|
+
nativeBinding = require('@kln-ctrl/mobile-core-android-arm-eabi')
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
loadError = e
|
|
56
|
+
}
|
|
57
|
+
break
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`Unsupported architecture on Android ${arch}`)
|
|
60
|
+
}
|
|
61
|
+
break
|
|
62
|
+
case 'win32':
|
|
63
|
+
switch (arch) {
|
|
64
|
+
case 'x64':
|
|
65
|
+
localFileExisted = existsSync(
|
|
66
|
+
join(__dirname, 'kln_ctrl_core.win32-x64-msvc.node')
|
|
67
|
+
)
|
|
68
|
+
try {
|
|
69
|
+
if (localFileExisted) {
|
|
70
|
+
nativeBinding = require('./kln_ctrl_core.win32-x64-msvc.node')
|
|
71
|
+
} else {
|
|
72
|
+
nativeBinding = require('@kln-ctrl/mobile-core-win32-x64-msvc')
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
loadError = e
|
|
76
|
+
}
|
|
77
|
+
break
|
|
78
|
+
case 'ia32':
|
|
79
|
+
localFileExisted = existsSync(
|
|
80
|
+
join(__dirname, 'kln_ctrl_core.win32-ia32-msvc.node')
|
|
81
|
+
)
|
|
82
|
+
try {
|
|
83
|
+
if (localFileExisted) {
|
|
84
|
+
nativeBinding = require('./kln_ctrl_core.win32-ia32-msvc.node')
|
|
85
|
+
} else {
|
|
86
|
+
nativeBinding = require('@kln-ctrl/mobile-core-win32-ia32-msvc')
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
loadError = e
|
|
90
|
+
}
|
|
91
|
+
break
|
|
92
|
+
case 'arm64':
|
|
93
|
+
localFileExisted = existsSync(
|
|
94
|
+
join(__dirname, 'kln_ctrl_core.win32-arm64-msvc.node')
|
|
95
|
+
)
|
|
96
|
+
try {
|
|
97
|
+
if (localFileExisted) {
|
|
98
|
+
nativeBinding = require('./kln_ctrl_core.win32-arm64-msvc.node')
|
|
99
|
+
} else {
|
|
100
|
+
nativeBinding = require('@kln-ctrl/mobile-core-win32-arm64-msvc')
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
loadError = e
|
|
104
|
+
}
|
|
105
|
+
break
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
|
108
|
+
}
|
|
109
|
+
break
|
|
110
|
+
case 'darwin':
|
|
111
|
+
localFileExisted = existsSync(join(__dirname, 'kln_ctrl_core.darwin-universal.node'))
|
|
112
|
+
try {
|
|
113
|
+
if (localFileExisted) {
|
|
114
|
+
nativeBinding = require('./kln_ctrl_core.darwin-universal.node')
|
|
115
|
+
} else {
|
|
116
|
+
nativeBinding = require('@kln-ctrl/mobile-core-darwin-universal')
|
|
117
|
+
}
|
|
118
|
+
break
|
|
119
|
+
} catch {}
|
|
120
|
+
switch (arch) {
|
|
121
|
+
case 'x64':
|
|
122
|
+
localFileExisted = existsSync(join(__dirname, 'kln_ctrl_core.darwin-x64.node'))
|
|
123
|
+
try {
|
|
124
|
+
if (localFileExisted) {
|
|
125
|
+
nativeBinding = require('./kln_ctrl_core.darwin-x64.node')
|
|
126
|
+
} else {
|
|
127
|
+
nativeBinding = require('@kln-ctrl/mobile-core-darwin-x64')
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
loadError = e
|
|
131
|
+
}
|
|
132
|
+
break
|
|
133
|
+
case 'arm64':
|
|
134
|
+
localFileExisted = existsSync(
|
|
135
|
+
join(__dirname, 'kln_ctrl_core.darwin-arm64.node')
|
|
136
|
+
)
|
|
137
|
+
try {
|
|
138
|
+
if (localFileExisted) {
|
|
139
|
+
nativeBinding = require('./kln_ctrl_core.darwin-arm64.node')
|
|
140
|
+
} else {
|
|
141
|
+
nativeBinding = require('@kln-ctrl/mobile-core-darwin-arm64')
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
loadError = e
|
|
145
|
+
}
|
|
146
|
+
break
|
|
147
|
+
default:
|
|
148
|
+
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
|
149
|
+
}
|
|
150
|
+
break
|
|
151
|
+
case 'freebsd':
|
|
152
|
+
if (arch !== 'x64') {
|
|
153
|
+
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
|
154
|
+
}
|
|
155
|
+
localFileExisted = existsSync(join(__dirname, 'kln_ctrl_core.freebsd-x64.node'))
|
|
156
|
+
try {
|
|
157
|
+
if (localFileExisted) {
|
|
158
|
+
nativeBinding = require('./kln_ctrl_core.freebsd-x64.node')
|
|
159
|
+
} else {
|
|
160
|
+
nativeBinding = require('@kln-ctrl/mobile-core-freebsd-x64')
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
loadError = e
|
|
164
|
+
}
|
|
165
|
+
break
|
|
166
|
+
case 'linux':
|
|
167
|
+
switch (arch) {
|
|
168
|
+
case 'x64':
|
|
169
|
+
if (isMusl()) {
|
|
170
|
+
localFileExisted = existsSync(
|
|
171
|
+
join(__dirname, 'kln_ctrl_core.linux-x64-musl.node')
|
|
172
|
+
)
|
|
173
|
+
try {
|
|
174
|
+
if (localFileExisted) {
|
|
175
|
+
nativeBinding = require('./kln_ctrl_core.linux-x64-musl.node')
|
|
176
|
+
} else {
|
|
177
|
+
nativeBinding = require('@kln-ctrl/mobile-core-linux-x64-musl')
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
loadError = e
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
localFileExisted = existsSync(
|
|
184
|
+
join(__dirname, 'kln_ctrl_core.linux-x64-gnu.node')
|
|
185
|
+
)
|
|
186
|
+
try {
|
|
187
|
+
if (localFileExisted) {
|
|
188
|
+
nativeBinding = require('./kln_ctrl_core.linux-x64-gnu.node')
|
|
189
|
+
} else {
|
|
190
|
+
nativeBinding = require('@kln-ctrl/mobile-core-linux-x64-gnu')
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
loadError = e
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
break
|
|
197
|
+
case 'arm64':
|
|
198
|
+
if (isMusl()) {
|
|
199
|
+
localFileExisted = existsSync(
|
|
200
|
+
join(__dirname, 'kln_ctrl_core.linux-arm64-musl.node')
|
|
201
|
+
)
|
|
202
|
+
try {
|
|
203
|
+
if (localFileExisted) {
|
|
204
|
+
nativeBinding = require('./kln_ctrl_core.linux-arm64-musl.node')
|
|
205
|
+
} else {
|
|
206
|
+
nativeBinding = require('@kln-ctrl/mobile-core-linux-arm64-musl')
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
loadError = e
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
localFileExisted = existsSync(
|
|
213
|
+
join(__dirname, 'kln_ctrl_core.linux-arm64-gnu.node')
|
|
214
|
+
)
|
|
215
|
+
try {
|
|
216
|
+
if (localFileExisted) {
|
|
217
|
+
nativeBinding = require('./kln_ctrl_core.linux-arm64-gnu.node')
|
|
218
|
+
} else {
|
|
219
|
+
nativeBinding = require('@kln-ctrl/mobile-core-linux-arm64-gnu')
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
loadError = e
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
break
|
|
226
|
+
case 'arm':
|
|
227
|
+
if (isMusl()) {
|
|
228
|
+
localFileExisted = existsSync(
|
|
229
|
+
join(__dirname, 'kln_ctrl_core.linux-arm-musleabihf.node')
|
|
230
|
+
)
|
|
231
|
+
try {
|
|
232
|
+
if (localFileExisted) {
|
|
233
|
+
nativeBinding = require('./kln_ctrl_core.linux-arm-musleabihf.node')
|
|
234
|
+
} else {
|
|
235
|
+
nativeBinding = require('@kln-ctrl/mobile-core-linux-arm-musleabihf')
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
loadError = e
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
localFileExisted = existsSync(
|
|
242
|
+
join(__dirname, 'kln_ctrl_core.linux-arm-gnueabihf.node')
|
|
243
|
+
)
|
|
244
|
+
try {
|
|
245
|
+
if (localFileExisted) {
|
|
246
|
+
nativeBinding = require('./kln_ctrl_core.linux-arm-gnueabihf.node')
|
|
247
|
+
} else {
|
|
248
|
+
nativeBinding = require('@kln-ctrl/mobile-core-linux-arm-gnueabihf')
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
loadError = e
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
break
|
|
255
|
+
case 'riscv64':
|
|
256
|
+
if (isMusl()) {
|
|
257
|
+
localFileExisted = existsSync(
|
|
258
|
+
join(__dirname, 'kln_ctrl_core.linux-riscv64-musl.node')
|
|
259
|
+
)
|
|
260
|
+
try {
|
|
261
|
+
if (localFileExisted) {
|
|
262
|
+
nativeBinding = require('./kln_ctrl_core.linux-riscv64-musl.node')
|
|
263
|
+
} else {
|
|
264
|
+
nativeBinding = require('@kln-ctrl/mobile-core-linux-riscv64-musl')
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
loadError = e
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
localFileExisted = existsSync(
|
|
271
|
+
join(__dirname, 'kln_ctrl_core.linux-riscv64-gnu.node')
|
|
272
|
+
)
|
|
273
|
+
try {
|
|
274
|
+
if (localFileExisted) {
|
|
275
|
+
nativeBinding = require('./kln_ctrl_core.linux-riscv64-gnu.node')
|
|
276
|
+
} else {
|
|
277
|
+
nativeBinding = require('@kln-ctrl/mobile-core-linux-riscv64-gnu')
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
loadError = e
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
break
|
|
284
|
+
case 's390x':
|
|
285
|
+
localFileExisted = existsSync(
|
|
286
|
+
join(__dirname, 'kln_ctrl_core.linux-s390x-gnu.node')
|
|
287
|
+
)
|
|
288
|
+
try {
|
|
289
|
+
if (localFileExisted) {
|
|
290
|
+
nativeBinding = require('./kln_ctrl_core.linux-s390x-gnu.node')
|
|
291
|
+
} else {
|
|
292
|
+
nativeBinding = require('@kln-ctrl/mobile-core-linux-s390x-gnu')
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
loadError = e
|
|
296
|
+
}
|
|
297
|
+
break
|
|
298
|
+
default:
|
|
299
|
+
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
|
300
|
+
}
|
|
301
|
+
break
|
|
302
|
+
default:
|
|
303
|
+
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!nativeBinding) {
|
|
307
|
+
if (loadError) {
|
|
308
|
+
throw loadError
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`Failed to load native binding`)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { KlnCoreClient } = nativeBinding
|
|
314
|
+
|
|
315
|
+
module.exports.KlnCoreClient = KlnCoreClient
|
package/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const cli = require('./cli/setup');
|
|
4
|
+
const wrapper = require('./wrapper');
|
|
5
|
+
const core = require('./core');
|
|
6
|
+
const config = require('./config');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
...wrapper,
|
|
10
|
+
core,
|
|
11
|
+
config,
|
|
12
|
+
run: cli.run,
|
|
13
|
+
parseArgs: cli.parseArgs
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
if (require.main === module) {
|
|
17
|
+
cli.run().then((code) => {
|
|
18
|
+
process.exitCode = code;
|
|
19
|
+
}).catch((error) => {
|
|
20
|
+
process.stderr.write(`${error.message || error}\n`);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
});
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kln-mcp/ctrl-mobile-cn",
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "KLN mobile control MCP server for OpenClaw (cn)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"kln-ctrl-mobile-install": "bin/install.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"cli/",
|
|
12
|
+
"config/",
|
|
13
|
+
"core/",
|
|
14
|
+
"skills/",
|
|
15
|
+
"wrapper/",
|
|
16
|
+
"index.js"
|
|
17
|
+
],
|
|
18
|
+
"optionalDependencies": {
|
|
19
|
+
"@kln-ctrl/mobile-core-darwin-arm64": "0.0.6",
|
|
20
|
+
"@kln-ctrl/mobile-core-darwin-x64": "0.0.6",
|
|
21
|
+
"@kln-ctrl/mobile-core-linux-x64-gnu": "0.0.6",
|
|
22
|
+
"@kln-ctrl/mobile-core-linux-arm64-gnu": "0.0.6",
|
|
23
|
+
"@kln-ctrl/mobile-core-win32-x64-msvc": "0.0.6"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# kln-mobile-ctrl
|
|
2
|
+
|
|
3
|
+
当用户要求 OpenClaw 操作 Android 手机、查看手机屏幕、打开或控制 App、点击/滑动/输入、从移动 App 采集数据,或提到“在我手机上”“帮我点开”“看下 App 里”“截屏”“继续上次”等表达时,使用本 skill。
|
|
4
|
+
|
|
5
|
+
## 标准循环
|
|
6
|
+
|
|
7
|
+
处理任何手机控制任务时,必须按以下循环执行:
|
|
8
|
+
|
|
9
|
+
1. 调用 `kln_get_device_context` 观察当前 App、页面提示、屏幕边界和精简后的可点击元素。
|
|
10
|
+
2. 判断当前页面,并只决定下一步的一个动作。
|
|
11
|
+
3. 使用 `kln_send_command`、`kln_send_text` 或输入类工具执行一个动作。
|
|
12
|
+
4. 动作后必须再次调用 `kln_get_device_context` 确认效果。
|
|
13
|
+
5. 严禁在两次观察之间连续执行多个手机动作。
|
|
14
|
+
6. 如果同一个动作连续 2 次没有明显效果,停止操作并询问用户下一步。
|
|
15
|
+
|
|
16
|
+
优先使用 `kln_get_device_context` 返回的元素编号和 bounds。若精简上下文不足以判断目标,不要猜,先问用户确认。
|
|
17
|
+
|
|
18
|
+
## 工具说明
|
|
19
|
+
|
|
20
|
+
- `kln_get_device_context`:返回精简 JSON,包含 `app`、`page_hint`、`screen_bounds`、`elements`,以及可选的 `truncated` 和 `screenshot_ref`。
|
|
21
|
+
- `kln_send_command`:发送一个原子命令。Android 端支持时,常见命令包括 `tap`、`long_press`、`swipe`、`back`、`open_app`、`wait_idle`。
|
|
22
|
+
- `kln_send_text`:向当前焦点输入框输入文本。
|
|
23
|
+
- `kln_input_tap`、`kln_input_swipe`、`kln_input_text`、`kln_input_clear`、`kln_input_key`:可用时优先使用这些结构化输入工具。
|
|
24
|
+
- `kln_start_screen_stream`:仅用于调试。常规 agent 循环不要依赖投屏流。
|
|
25
|
+
- `kln_task_memory`:本地任务进度 memory,`op` 支持 `list`、`get`、`put`、`delete`。
|
|
26
|
+
|
|
27
|
+
## 数据采集任务
|
|
28
|
+
|
|
29
|
+
长任务需要阶段性写入进度:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"op": "put",
|
|
34
|
+
"key": "tiktok-food-20260518",
|
|
35
|
+
"summary": "TikTok 今日美食爆款抓取",
|
|
36
|
+
"progress": "已收集 12/30 条",
|
|
37
|
+
"value": "{\"collected\":[],\"cursor\":\"feed-page-3\"}"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
key 使用 `<domain>-<topic>-<YYYYMMDD>` 格式,例如:
|
|
42
|
+
|
|
43
|
+
- `tiktok-food-20260518`
|
|
44
|
+
- `youtube-food-20260518`
|
|
45
|
+
- `taobao-sneakers-20260518`
|
|
46
|
+
|
|
47
|
+
每次 `put` 都必须提供 `summary` 和 `progress`。`value` 是不透明 JSON 字符串。不要把账号密码、短信验证码、私有 token 或其他敏感信息写入 task memory。
|
|
48
|
+
|
|
49
|
+
任务完成后,用 JSON 或紧凑的表格式摘要回复用户。采集结果不要只写散文。
|
|
50
|
+
|
|
51
|
+
## 任务续接强制流程
|
|
52
|
+
|
|
53
|
+
当用户表达续接意图,例如“继续”“接着”“上次那个”“还没看完”“恢复”时:
|
|
54
|
+
|
|
55
|
+
### Step 1:必须先 list
|
|
56
|
+
|
|
57
|
+
立即调用:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{ "op": "list" }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
不要根据用户话术猜 key。
|
|
64
|
+
|
|
65
|
+
### Step 2:必须让用户确认
|
|
66
|
+
|
|
67
|
+
哪怕只有 1 个相关任务,也必须列出来等用户确认。选错 memory 会污染后续采集数据。
|
|
68
|
+
|
|
69
|
+
多匹配样板:
|
|
70
|
+
|
|
71
|
+
> 我看到这些进行中的任务,要继续哪个?
|
|
72
|
+
>
|
|
73
|
+
> [1] TikTok 今日美食爆款 - 已 12/30,2 小时前
|
|
74
|
+
> [2] YouTube 今日美食爆款 - 已 5/20,昨天
|
|
75
|
+
>
|
|
76
|
+
> 回复编号,或说“都不是,新开任务”。
|
|
77
|
+
|
|
78
|
+
单匹配样板:
|
|
79
|
+
|
|
80
|
+
> 找到 1 个相关任务:
|
|
81
|
+
> “TikTok 今日美食爆款” - 已 12/30,2 小时前
|
|
82
|
+
>
|
|
83
|
+
> 是要继续这个,还是新开任务?
|
|
84
|
+
|
|
85
|
+
零匹配样板:
|
|
86
|
+
|
|
87
|
+
> 没找到相关未完成任务,我作为新任务开始,请确认目标:...
|
|
88
|
+
|
|
89
|
+
### Step 3:确认前不要继续
|
|
90
|
+
|
|
91
|
+
在用户明确回复编号或确认“是”之前,不要调用 `kln_task_memory` 的 `get`,也不要操作手机。
|
|
92
|
+
|
|
93
|
+
### Step 4:确认后再恢复
|
|
94
|
+
|
|
95
|
+
用户确认后:
|
|
96
|
+
|
|
97
|
+
1. 调用 `kln_task_memory`,使用 `op: "get"` 和确认后的 key。
|
|
98
|
+
2. 调用 `kln_get_device_context` 查看手机当前真实状态。
|
|
99
|
+
3. 从观察到的当前屏幕继续标准循环,不要只依赖 memory 里的进度。
|
|
100
|
+
|
|
101
|
+
## 示例
|
|
102
|
+
|
|
103
|
+
用户:“帮我看下今天 TikTok 美食爆款视频”
|
|
104
|
+
|
|
105
|
+
助手:
|
|
106
|
+
|
|
107
|
+
1. 调用 `kln_get_device_context`。
|
|
108
|
+
2. 如果 TikTok 未打开,调用 `kln_app_start` 或用 `kln_send_command` 执行 `open_app`。
|
|
109
|
+
3. 再次观察。
|
|
110
|
+
4. 每次只搜索或导航一步。
|
|
111
|
+
5. 有阶段性进展后,调用 `kln_task_memory`,key 例如 `tiktok-food-20260518`。
|
|
112
|
+
6. 最终用 JSON 输出采集结果。
|
|
113
|
+
|
|
114
|
+
用户:“继续看美食爆款”
|
|
115
|
+
|
|
116
|
+
助手:
|
|
117
|
+
|
|
118
|
+
1. `kln_task_memory({ "op": "list" })`
|
|
119
|
+
2. 展示候选并等待用户确认。
|
|
120
|
+
3. 用户确认后才调用 `kln_task_memory({ "op": "get", "key": "..." })`
|
|
121
|
+
4. 用 `kln_get_device_context` 观察手机。
|
|
122
|
+
5. 继续执行“一次观察,一个动作,再观察”的循环。
|