@11agents/cli 0.1.24 → 0.1.26
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 +52 -0
- package/bin/11agents.js +12 -0
- package/mobile-runtime/README.md +19 -0
- package/mobile-runtime/configs/platforms/xiaohongshu_d01.json +73 -0
- package/mobile-runtime/configs/platforms/xiaohongshu_d02.json +70 -0
- package/mobile-runtime/configs/platforms/xiaohongshu_d03.json +73 -0
- package/mobile-runtime/configs/publish_policy.json +40 -0
- package/mobile-runtime/data-templates/README.md +4 -0
- package/mobile-runtime/data-templates/accounts.example.csv +6 -0
- package/mobile-runtime/data-templates/devices.example.csv +2 -0
- package/mobile-runtime/data-templates/publish_records.example.jsonl +2 -0
- package/mobile-runtime/data-templates/tasks.example.jsonl +5 -0
- package/mobile-runtime/data-templates/video_metrics.example.jsonl +1 -0
- package/mobile-runtime/python/pyproject.toml +34 -0
- package/mobile-runtime/python/src/device_control/__init__.py +5 -0
- package/mobile-runtime/python/src/device_control/adapters/__init__.py +31 -0
- package/mobile-runtime/python/src/device_control/adapters/base.py +43 -0
- package/mobile-runtime/python/src/device_control/adapters/facebook.py +30 -0
- package/mobile-runtime/python/src/device_control/adapters/instagram.py +25 -0
- package/mobile-runtime/python/src/device_control/adapters/reddit.py +29 -0
- package/mobile-runtime/python/src/device_control/adapters/tiktok.py +25 -0
- package/mobile-runtime/python/src/device_control/adapters/x.py +29 -0
- package/mobile-runtime/python/src/device_control/adapters/xiaohongshu.py +26 -0
- package/mobile-runtime/python/src/device_control/adb.py +161 -0
- package/mobile-runtime/python/src/device_control/appium_client.py +131 -0
- package/mobile-runtime/python/src/device_control/appium_manager.py +403 -0
- package/mobile-runtime/python/src/device_control/cli.py +1608 -0
- package/mobile-runtime/python/src/device_control/entrypoints.py +60 -0
- package/mobile-runtime/python/src/device_control/locks.py +162 -0
- package/mobile-runtime/python/src/device_control/metrics/__init__.py +33 -0
- package/mobile-runtime/python/src/device_control/metrics/collectors.py +320 -0
- package/mobile-runtime/python/src/device_control/metrics/tiktok_account_adb.py +367 -0
- package/mobile-runtime/python/src/device_control/metrics/tiktok_video_adb.py +714 -0
- package/mobile-runtime/python/src/device_control/models.py +439 -0
- package/mobile-runtime/python/src/device_control/publish_policy.py +173 -0
- package/mobile-runtime/python/src/device_control/publishers/__init__.py +24 -0
- package/mobile-runtime/python/src/device_control/publishers/facebook_adb.py +494 -0
- package/mobile-runtime/python/src/device_control/publishers/instagram_adb.py +663 -0
- package/mobile-runtime/python/src/device_control/publishers/reddit_adb.py +595 -0
- package/mobile-runtime/python/src/device_control/publishers/tiktok_adb.py +477 -0
- package/mobile-runtime/python/src/device_control/publishers/tiktok_appium.py +259 -0
- package/mobile-runtime/python/src/device_control/publishers/ui_helpers.py +372 -0
- package/mobile-runtime/python/src/device_control/publishers/x_adb.py +636 -0
- package/mobile-runtime/python/src/device_control/publishers/xiaohongshu_adb.py +1143 -0
- package/mobile-runtime/python/src/device_control/store.py +137 -0
- package/mobile-runtime/scripts/appium_smoke.py +71 -0
- package/mobile-runtime/skills/android-collect-tiktok-metrics/SKILL.md +60 -0
- package/mobile-runtime/skills/android-collect-tiktok-metrics/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-group-control-cli/SKILL.md +76 -0
- package/mobile-runtime/skills/android-group-control-cli/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-group-control-cli/references/command-reference.md +122 -0
- package/mobile-runtime/skills/android-publish-facebook/SKILL.md +41 -0
- package/mobile-runtime/skills/android-publish-facebook/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-publish-instagram/SKILL.md +45 -0
- package/mobile-runtime/skills/android-publish-instagram/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-publish-reddit/SKILL.md +41 -0
- package/mobile-runtime/skills/android-publish-reddit/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-publish-tiktok/SKILL.md +43 -0
- package/mobile-runtime/skills/android-publish-tiktok/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-publish-x/SKILL.md +40 -0
- package/mobile-runtime/skills/android-publish-x/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-publish-xiaohongshu/SKILL.md +50 -0
- package/mobile-runtime/skills/android-publish-xiaohongshu/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/mobile-publish-data-collection/SKILL.md +49 -0
- package/mobile-runtime/skills/mobile-publish-device-health/SKILL.md +47 -0
- package/mobile-runtime/skills/mobile-publish-execution/SKILL.md +57 -0
- package/mobile-runtime/skills/mobile-publish-records/SKILL.md +29 -0
- package/package.json +4 -1
- package/scripts/mobile-postinstall.js +26 -0
- package/src/commands/mobile.js +695 -0
- package/src/commands/runtime.js +21 -5
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { constants } from 'node:fs'
|
|
3
|
+
import { access, copyFile, mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { flag } from '../args.js'
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const CLI_ROOT = path.resolve(__dirname, '..', '..')
|
|
11
|
+
const MOBILE_RUNTIME_ROOT = path.join(CLI_ROOT, 'mobile-runtime')
|
|
12
|
+
const MOBILE_PYTHON_ROOT = path.join(MOBILE_RUNTIME_ROOT, 'python')
|
|
13
|
+
const MOBILE_CONFIG_ROOT = path.join(MOBILE_RUNTIME_ROOT, 'configs')
|
|
14
|
+
const MOBILE_DATA_TEMPLATE_ROOT = path.join(MOBILE_RUNTIME_ROOT, 'data-templates')
|
|
15
|
+
const MOBILE_SKILLS_ROOT = path.join(MOBILE_RUNTIME_ROOT, 'skills')
|
|
16
|
+
const MOBILE_SCRIPTS_ROOT = path.join(MOBILE_RUNTIME_ROOT, 'scripts')
|
|
17
|
+
|
|
18
|
+
const PYTHON_COMMANDS = new Set([
|
|
19
|
+
'doctor',
|
|
20
|
+
'list-adb',
|
|
21
|
+
'ensure-appium',
|
|
22
|
+
'stop-appium',
|
|
23
|
+
'list-devices',
|
|
24
|
+
'init-one-device',
|
|
25
|
+
'connect',
|
|
26
|
+
'health',
|
|
27
|
+
'screenshot',
|
|
28
|
+
'dump-ui',
|
|
29
|
+
'tap',
|
|
30
|
+
'keyevent',
|
|
31
|
+
'input-text',
|
|
32
|
+
'launch',
|
|
33
|
+
'plan-task',
|
|
34
|
+
'record-publish',
|
|
35
|
+
'list-publish-records',
|
|
36
|
+
'update-publish-record',
|
|
37
|
+
'add-metrics',
|
|
38
|
+
'collect-metrics',
|
|
39
|
+
'list-metrics',
|
|
40
|
+
'collect-tiktok-account-metrics-adb',
|
|
41
|
+
'list-tiktok-account-metrics',
|
|
42
|
+
'collect-tiktok-video-metrics-adb',
|
|
43
|
+
'list-tiktok-video-metrics',
|
|
44
|
+
'publish-tiktok-adb',
|
|
45
|
+
'publish-tiktok',
|
|
46
|
+
'publish-tiktok-appium',
|
|
47
|
+
'publish-reddit-adb',
|
|
48
|
+
'publish-reddit',
|
|
49
|
+
'publish-facebook',
|
|
50
|
+
'publish-instagram',
|
|
51
|
+
'publish-x',
|
|
52
|
+
'publish-xiaohongshu',
|
|
53
|
+
'copy-xiaohongshu-link',
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
const SCRIPT_COMMANDS = new Map([
|
|
57
|
+
['appium-smoke', 'appium_smoke.py'],
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
const COMMAND_ALIASES = new Map([
|
|
61
|
+
['collect-tiktok-account-metrics', 'collect-tiktok-account-metrics-adb'],
|
|
62
|
+
['collect-tiktok-video-metrics', 'collect-tiktok-video-metrics-adb'],
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
const WRAPPER_FLAG_NAMES = new Set([
|
|
66
|
+
'mobile-home',
|
|
67
|
+
'task-id',
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
const EMPTY_DATA_FILES = [
|
|
71
|
+
'publish_records.jsonl',
|
|
72
|
+
'video_metrics.jsonl',
|
|
73
|
+
'tiktok_account_metrics.jsonl',
|
|
74
|
+
'tiktok_video_metrics.jsonl',
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
function usage() {
|
|
78
|
+
console.log(`11agents mobile
|
|
79
|
+
|
|
80
|
+
Usage:
|
|
81
|
+
11agents mobile help
|
|
82
|
+
11agents mobile setup [--json] [--skip-python] [--skip-npm]
|
|
83
|
+
11agents mobile doctor [--json]
|
|
84
|
+
11agents mobile migrate --from <android-group-control-mvp>
|
|
85
|
+
11agents mobile skills list
|
|
86
|
+
11agents mobile skills show <skill-name>
|
|
87
|
+
11agents mobile data collect --platform <x|reddit|instagram|facebook|tiktok|xiaohongshu> [--scope account|video|record] [--record-id <id>] [--device Dxx]
|
|
88
|
+
11agents mobile <device-control-command> [args...]
|
|
89
|
+
11agents mobile appium-smoke --udid <adb-serial> [--server http://127.0.0.1:4723]
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
11agents mobile list-devices --health --json
|
|
93
|
+
11agents mobile publish-tiktok --device D03 --video /path/video.mp4 --caption "..." --json
|
|
94
|
+
11agents mobile collect-tiktok-video-metrics --device D03 --video-order 1 --json
|
|
95
|
+
|
|
96
|
+
Runtime:
|
|
97
|
+
Python/device-control code is bundled with @11agents/cli.
|
|
98
|
+
Mutable runtime state lives in ~/.11agents/mobile by default.
|
|
99
|
+
Every command writes logs under ~/.11agents/mobile/runs/<task_id>/log.`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mobileHome(flags = {}, env = process.env) {
|
|
103
|
+
return path.resolve(
|
|
104
|
+
flag(flags, 'mobile-home')
|
|
105
|
+
|| env.ELEVENAGENTS_MOBILE_HOME
|
|
106
|
+
|| path.join(os.homedir(), '.11agents', 'mobile')
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function taskIdFrom(flags = {}, env = process.env, now = new Date()) {
|
|
111
|
+
const explicit = flag(flags, 'task-id') || env.ELEVENAGENTS_TASK_ID || ''
|
|
112
|
+
if (explicit) return sanitizeTaskId(explicit)
|
|
113
|
+
const stamp = now.toISOString().replace(/[-:]/g, '').replace(/\..+$/, '').replace('T', '-')
|
|
114
|
+
const suffix = Math.random().toString(16).slice(2, 8)
|
|
115
|
+
return `manual-${stamp}-${suffix}`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sanitizeTaskId(value) {
|
|
119
|
+
return String(value || 'manual')
|
|
120
|
+
.trim()
|
|
121
|
+
.replace(/[^A-Za-z0-9_.-]+/g, '-')
|
|
122
|
+
.replace(/^-+|-+$/g, '')
|
|
123
|
+
.slice(0, 160) || 'manual'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseRawArgs(argv) {
|
|
127
|
+
const positional = []
|
|
128
|
+
const flags = {}
|
|
129
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
130
|
+
const arg = argv[i]
|
|
131
|
+
if (!arg.startsWith('--')) {
|
|
132
|
+
positional.push(arg)
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
const raw = arg.slice(2)
|
|
136
|
+
const eq = raw.indexOf('=')
|
|
137
|
+
if (eq >= 0) {
|
|
138
|
+
addFlag(flags, raw.slice(0, eq), raw.slice(eq + 1))
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
const key = raw
|
|
142
|
+
const next = argv[i + 1]
|
|
143
|
+
if (!next || next.startsWith('--')) {
|
|
144
|
+
addFlag(flags, key, true)
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
addFlag(flags, key, next)
|
|
148
|
+
i += 1
|
|
149
|
+
}
|
|
150
|
+
return { positional, flags }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function addFlag(flags, key, value) {
|
|
154
|
+
if (flags[key] === undefined) flags[key] = value
|
|
155
|
+
else if (Array.isArray(flags[key])) flags[key].push(value)
|
|
156
|
+
else flags[key] = [flags[key], value]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function stripWrapperFlags(args) {
|
|
160
|
+
const out = []
|
|
161
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
162
|
+
const arg = args[i]
|
|
163
|
+
if (!arg.startsWith('--')) {
|
|
164
|
+
out.push(arg)
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
const raw = arg.slice(2)
|
|
168
|
+
const eq = raw.indexOf('=')
|
|
169
|
+
const key = eq >= 0 ? raw.slice(0, eq) : raw
|
|
170
|
+
if (!WRAPPER_FLAG_NAMES.has(key)) {
|
|
171
|
+
out.push(arg)
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
if (eq < 0) {
|
|
175
|
+
const next = args[i + 1]
|
|
176
|
+
if (next && !next.startsWith('--')) i += 1
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function venvPython(home, platform = process.platform) {
|
|
183
|
+
return platform === 'win32'
|
|
184
|
+
? path.join(home, '.venv', 'Scripts', 'python.exe')
|
|
185
|
+
: path.join(home, '.venv', 'bin', 'python')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function exists(filePath, deps = {}) {
|
|
189
|
+
const accessFn = deps.access || access
|
|
190
|
+
try {
|
|
191
|
+
await accessFn(filePath, constants.F_OK)
|
|
192
|
+
return true
|
|
193
|
+
} catch {
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function isExecutable(filePath, deps = {}) {
|
|
199
|
+
const accessFn = deps.access || access
|
|
200
|
+
try {
|
|
201
|
+
await accessFn(filePath, constants.X_OK)
|
|
202
|
+
return true
|
|
203
|
+
} catch {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function copyDir(source, target, { overwrite = true } = {}) {
|
|
209
|
+
await mkdir(target, { recursive: true })
|
|
210
|
+
const entries = await readdir(source, { withFileTypes: true })
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
const from = path.join(source, entry.name)
|
|
213
|
+
const to = path.join(target, entry.name)
|
|
214
|
+
if (entry.isDirectory()) {
|
|
215
|
+
await copyDir(from, to, { overwrite })
|
|
216
|
+
} else if (entry.isFile()) {
|
|
217
|
+
await mkdir(path.dirname(to), { recursive: true })
|
|
218
|
+
if (!overwrite && await exists(to)) continue
|
|
219
|
+
await copyFile(from, to)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function writeFileIfMissing(filePath, content) {
|
|
225
|
+
try {
|
|
226
|
+
await access(filePath, constants.F_OK)
|
|
227
|
+
} catch {
|
|
228
|
+
await mkdir(path.dirname(filePath), { recursive: true })
|
|
229
|
+
await writeFile(filePath, content)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function scaffoldMobileHome(home) {
|
|
234
|
+
await mkdir(home, { recursive: true })
|
|
235
|
+
await mkdir(path.join(home, 'data'), { recursive: true })
|
|
236
|
+
await mkdir(path.join(home, 'artifacts', 'screenshots'), { recursive: true })
|
|
237
|
+
await mkdir(path.join(home, 'artifacts', 'ui'), { recursive: true })
|
|
238
|
+
await mkdir(path.join(home, 'runs'), { recursive: true })
|
|
239
|
+
await copyDir(MOBILE_CONFIG_ROOT, path.join(home, 'configs'), { overwrite: false })
|
|
240
|
+
await copyDir(MOBILE_SKILLS_ROOT, path.join(home, 'skills'), { overwrite: false })
|
|
241
|
+
|
|
242
|
+
await writeFileIfMissing(
|
|
243
|
+
path.join(home, 'data', 'accounts.csv'),
|
|
244
|
+
await readFile(path.join(MOBILE_DATA_TEMPLATE_ROOT, 'accounts.example.csv'), 'utf8')
|
|
245
|
+
)
|
|
246
|
+
await writeFileIfMissing(
|
|
247
|
+
path.join(home, 'data', 'devices.csv'),
|
|
248
|
+
await readFile(path.join(MOBILE_DATA_TEMPLATE_ROOT, 'devices.example.csv'), 'utf8')
|
|
249
|
+
)
|
|
250
|
+
for (const filename of EMPTY_DATA_FILES) {
|
|
251
|
+
await writeFileIfMissing(path.join(home, 'data', filename), '')
|
|
252
|
+
}
|
|
253
|
+
await writeFileIfMissing(path.join(home, 'data', 'tasks.jsonl'), '')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function runProcess(command, args, { cwd = process.cwd(), env = process.env, input = '', onStdoutChunk, onStderrChunk } = {}) {
|
|
257
|
+
return new Promise(resolve => {
|
|
258
|
+
const child = spawn(command, args, { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
259
|
+
let stdout = ''
|
|
260
|
+
let stderr = ''
|
|
261
|
+
child.stdout.on('data', chunk => {
|
|
262
|
+
stdout += chunk
|
|
263
|
+
onStdoutChunk?.(chunk)
|
|
264
|
+
})
|
|
265
|
+
child.stderr.on('data', chunk => {
|
|
266
|
+
stderr += chunk
|
|
267
|
+
onStderrChunk?.(chunk)
|
|
268
|
+
})
|
|
269
|
+
child.on('error', error => {
|
|
270
|
+
resolve({ code: 127, stdout, stderr: stderr || error.message })
|
|
271
|
+
})
|
|
272
|
+
child.on('close', code => {
|
|
273
|
+
resolve({ code: code ?? 1, stdout, stderr })
|
|
274
|
+
})
|
|
275
|
+
child.stdin.end(input)
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function runAndLog({ command, args, cwd, env, runDir, log, deps = {} }) {
|
|
280
|
+
const run = deps.runProcess || runProcess
|
|
281
|
+
await mkdir(runDir, { recursive: true })
|
|
282
|
+
const stdoutPath = path.join(runDir, 'stdout.log')
|
|
283
|
+
const stderrPath = path.join(runDir, 'stderr.log')
|
|
284
|
+
const commandPath = path.join(runDir, 'command.json')
|
|
285
|
+
await writeFile(commandPath, JSON.stringify({
|
|
286
|
+
command,
|
|
287
|
+
args,
|
|
288
|
+
cwd,
|
|
289
|
+
started_at: new Date().toISOString(),
|
|
290
|
+
}, null, 2))
|
|
291
|
+
await appendLog(log, `command: ${[command, ...args].map(shellQuote).join(' ')}\n`)
|
|
292
|
+
await appendLog(log, `cwd: ${cwd}\n`)
|
|
293
|
+
const result = await run(command, args, {
|
|
294
|
+
cwd,
|
|
295
|
+
env,
|
|
296
|
+
onStdoutChunk: chunk => appendFileBestEffort(stdoutPath, chunk),
|
|
297
|
+
onStderrChunk: chunk => appendFileBestEffort(stderrPath, chunk),
|
|
298
|
+
})
|
|
299
|
+
await writeFile(stdoutPath, result.stdout || '')
|
|
300
|
+
await writeFile(stderrPath, result.stderr || '')
|
|
301
|
+
await appendLog(log, `exit_code: ${result.code}\n`)
|
|
302
|
+
if (result.stdout) await appendLog(log, `stdout:\n${result.stdout}\n`)
|
|
303
|
+
if (result.stderr) await appendLog(log, `stderr:\n${result.stderr}\n`)
|
|
304
|
+
return result
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function appendFileBestEffort(filePath, chunk) {
|
|
308
|
+
try {
|
|
309
|
+
await mkdir(path.dirname(filePath), { recursive: true })
|
|
310
|
+
await writeFile(filePath, chunk, { flag: 'a' })
|
|
311
|
+
} catch {}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function appendLog(logPath, text) {
|
|
315
|
+
await mkdir(path.dirname(logPath), { recursive: true })
|
|
316
|
+
await writeFile(logPath, text, { flag: 'a' })
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function shellQuote(value) {
|
|
320
|
+
const text = String(value)
|
|
321
|
+
if (/^[A-Za-z0-9_./:=@-]+$/.test(text)) return text
|
|
322
|
+
return JSON.stringify(text)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function pythonEnv(home, env = process.env) {
|
|
326
|
+
const appiumBin = path.join(home, 'node_modules', '.bin', process.platform === 'win32' ? 'appium.cmd' : 'appium')
|
|
327
|
+
return {
|
|
328
|
+
...env,
|
|
329
|
+
PYTHONPATH: [
|
|
330
|
+
path.join(MOBILE_PYTHON_ROOT, 'src'),
|
|
331
|
+
env.PYTHONPATH || '',
|
|
332
|
+
].filter(Boolean).join(path.delimiter),
|
|
333
|
+
APPIUM_BIN: env.APPIUM_BIN || appiumBin,
|
|
334
|
+
APPIUM_HOME: env.APPIUM_HOME || path.join(home, 'appium'),
|
|
335
|
+
ELEVENAGENTS_MOBILE_HOME: home,
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function mobileDoctor(flags = {}, deps = {}) {
|
|
340
|
+
const home = mobileHome(flags, deps.env || process.env)
|
|
341
|
+
const python = venvPython(home, deps.platform || process.platform)
|
|
342
|
+
const checks = {
|
|
343
|
+
mobile_home: home,
|
|
344
|
+
bundled_python_runtime: await exists(path.join(MOBILE_PYTHON_ROOT, 'src', 'device_control', 'cli.py'), deps),
|
|
345
|
+
runtime_configs: await exists(MOBILE_CONFIG_ROOT, deps),
|
|
346
|
+
bundled_skills: await exists(path.join(MOBILE_SKILLS_ROOT, 'android-group-control-cli', 'SKILL.md'), deps),
|
|
347
|
+
home_exists: await exists(home, deps),
|
|
348
|
+
venv_python: await isExecutable(python, deps),
|
|
349
|
+
data_devices_csv: await exists(path.join(home, 'data', 'devices.csv'), deps),
|
|
350
|
+
publish_policy: await exists(path.join(home, 'configs', 'publish_policy.json'), deps),
|
|
351
|
+
appium_bin: await isExecutable(path.join(home, 'node_modules', '.bin', process.platform === 'win32' ? 'appium.cmd' : 'appium'), deps),
|
|
352
|
+
appium_home: await exists(path.join(home, 'appium'), deps),
|
|
353
|
+
}
|
|
354
|
+
const ok = checks.bundled_python_runtime && checks.runtime_configs && checks.bundled_skills && checks.home_exists && checks.venv_python
|
|
355
|
+
const result = {
|
|
356
|
+
ok,
|
|
357
|
+
status: ok ? 'ready' : 'needs_setup',
|
|
358
|
+
setup_command: '11agents mobile setup',
|
|
359
|
+
checks,
|
|
360
|
+
}
|
|
361
|
+
if (flags.json) {
|
|
362
|
+
;(deps.log || console.log)(JSON.stringify(result, null, 2))
|
|
363
|
+
} else {
|
|
364
|
+
;(deps.log || console.log)([
|
|
365
|
+
`status=${result.status}`,
|
|
366
|
+
`mobile_home=${home}`,
|
|
367
|
+
`setup_command=${result.setup_command}`,
|
|
368
|
+
].join('\n'))
|
|
369
|
+
}
|
|
370
|
+
return result
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function mobileSetup(flags = {}, deps = {}) {
|
|
374
|
+
const env = deps.env || process.env
|
|
375
|
+
const home = mobileHome(flags, env)
|
|
376
|
+
await scaffoldMobileHome(home)
|
|
377
|
+
const python = venvPython(home, deps.platform || process.platform)
|
|
378
|
+
const run = deps.runProcess || runProcess
|
|
379
|
+
const steps = []
|
|
380
|
+
|
|
381
|
+
if (!flags['skip-python']) {
|
|
382
|
+
if (!await exists(python, deps)) {
|
|
383
|
+
const venvResult = await run(env.PYTHON || env.PYTHON3 || 'python3', ['-m', 'venv', path.join(home, '.venv')], { cwd: home, env })
|
|
384
|
+
steps.push({ step: 'python venv', code: venvResult.code, stderr: venvResult.stderr })
|
|
385
|
+
if (venvResult.code !== 0) throw new Error(`python venv failed: ${venvResult.stderr || venvResult.stdout}`)
|
|
386
|
+
}
|
|
387
|
+
const pipResult = await run(python, ['-m', 'pip', 'install', '-U', 'pip'], { cwd: home, env: pythonEnv(home, env) })
|
|
388
|
+
steps.push({ step: 'pip upgrade', code: pipResult.code, stderr: pipResult.stderr })
|
|
389
|
+
if (pipResult.code !== 0) throw new Error(`pip upgrade failed: ${pipResult.stderr || pipResult.stdout}`)
|
|
390
|
+
const installResult = await run(python, ['-m', 'pip', 'install', '-e', `${MOBILE_PYTHON_ROOT}[appium]`], { cwd: home, env: pythonEnv(home, env) })
|
|
391
|
+
steps.push({ step: 'mobile python install', code: installResult.code, stderr: installResult.stderr })
|
|
392
|
+
if (installResult.code !== 0) throw new Error(`mobile runtime install failed: ${installResult.stderr || installResult.stdout}`)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!flags['skip-npm']) {
|
|
396
|
+
await writeFileIfMissing(path.join(home, 'package.json'), JSON.stringify({
|
|
397
|
+
private: true,
|
|
398
|
+
name: 'elevenagents-mobile-runtime-local',
|
|
399
|
+
version: '0.0.0',
|
|
400
|
+
dependencies: {
|
|
401
|
+
appium: '^3.4.2',
|
|
402
|
+
'appium-uiautomator2-driver': '^7.5.2',
|
|
403
|
+
},
|
|
404
|
+
}, null, 2))
|
|
405
|
+
const npmResult = await run(env.NPM || 'npm', ['install', '--prefix', home], { cwd: home, env })
|
|
406
|
+
steps.push({ step: 'appium npm install', code: npmResult.code, stderr: npmResult.stderr })
|
|
407
|
+
if (npmResult.code !== 0) throw new Error(`appium npm install failed: ${npmResult.stderr || npmResult.stdout}`)
|
|
408
|
+
const appiumCli = path.join(home, 'node_modules', '.bin', process.platform === 'win32' ? 'appium.cmd' : 'appium')
|
|
409
|
+
const appiumHome = path.join(home, 'appium')
|
|
410
|
+
await mkdir(appiumHome, { recursive: true })
|
|
411
|
+
const appiumEnv = { ...env, APPIUM_HOME: env.APPIUM_HOME || appiumHome }
|
|
412
|
+
const driverResult = await run(appiumCli, ['driver', 'install', 'uiautomator2'], { cwd: home, env: appiumEnv })
|
|
413
|
+
steps.push({ step: 'appium uiautomator2 driver', code: driverResult.code, stderr: driverResult.stderr })
|
|
414
|
+
if (driverResult.code !== 0 && !/already installed|already exists/i.test(`${driverResult.stdout}\n${driverResult.stderr}`)) {
|
|
415
|
+
throw new Error(`appium uiautomator2 driver install failed: ${driverResult.stderr || driverResult.stdout}`)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const result = {
|
|
420
|
+
ok: true,
|
|
421
|
+
status: 'ready',
|
|
422
|
+
mobile_home: home,
|
|
423
|
+
python,
|
|
424
|
+
steps,
|
|
425
|
+
}
|
|
426
|
+
;(deps.log || console.log)(flags.json ? JSON.stringify(result, null, 2) : `mobile runtime ready: ${home}`)
|
|
427
|
+
return result
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function mobileMigrate(flags = {}, deps = {}) {
|
|
431
|
+
const from = path.resolve(flag(flags, 'from') || '')
|
|
432
|
+
if (!from) throw new Error('mobile migrate requires --from <android-group-control-mvp>')
|
|
433
|
+
if (!await exists(path.join(from, 'src', 'device_control', 'cli.py'), deps)) {
|
|
434
|
+
throw new Error(`not an android group-control checkout: ${from}`)
|
|
435
|
+
}
|
|
436
|
+
const home = mobileHome(flags, deps.env || process.env)
|
|
437
|
+
await scaffoldMobileHome(home)
|
|
438
|
+
await copyDir(path.join(from, 'configs'), path.join(home, 'configs'), { overwrite: true })
|
|
439
|
+
await mkdir(path.join(home, 'data'), { recursive: true })
|
|
440
|
+
for (const filename of [
|
|
441
|
+
'accounts.csv',
|
|
442
|
+
'devices.csv',
|
|
443
|
+
'publish_records.jsonl',
|
|
444
|
+
'video_metrics.jsonl',
|
|
445
|
+
'tiktok_account_metrics.jsonl',
|
|
446
|
+
'tiktok_video_metrics.jsonl',
|
|
447
|
+
'tasks.jsonl',
|
|
448
|
+
]) {
|
|
449
|
+
const source = path.join(from, 'data', filename)
|
|
450
|
+
if (await exists(source, deps)) await copyFile(source, path.join(home, 'data', filename))
|
|
451
|
+
}
|
|
452
|
+
const result = { ok: true, migrated_from: from, mobile_home: home }
|
|
453
|
+
;(deps.log || console.log)(flags.json ? JSON.stringify(result, null, 2) : `migrated mobile runtime data to ${home}`)
|
|
454
|
+
return result
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function mobileSkills(argv = [], deps = {}) {
|
|
458
|
+
const { positional } = parseRawArgs(argv)
|
|
459
|
+
const [subcommand, name] = positional
|
|
460
|
+
if (!subcommand || subcommand === 'list') {
|
|
461
|
+
const entries = await readdir(MOBILE_SKILLS_ROOT, { withFileTypes: true })
|
|
462
|
+
const names = entries.filter(entry => entry.isDirectory()).map(entry => entry.name).sort()
|
|
463
|
+
;(deps.log || console.log)(names.join('\n'))
|
|
464
|
+
return names
|
|
465
|
+
}
|
|
466
|
+
if (subcommand === 'show') {
|
|
467
|
+
if (!name) throw new Error('mobile skills show requires a skill name')
|
|
468
|
+
const filePath = path.join(MOBILE_SKILLS_ROOT, name, 'SKILL.md')
|
|
469
|
+
if (!await exists(filePath, deps)) throw new Error(`mobile skill not found: ${name}`)
|
|
470
|
+
const text = await readFile(filePath, 'utf8')
|
|
471
|
+
;(deps.log || console.log)(text)
|
|
472
|
+
return text
|
|
473
|
+
}
|
|
474
|
+
throw new Error(`unsupported mobile skills command: ${subcommand}`)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function ensureRunnable(home, deps = {}) {
|
|
478
|
+
await scaffoldMobileHome(home)
|
|
479
|
+
const python = venvPython(home, deps.platform || process.platform)
|
|
480
|
+
if (!await exists(python, deps)) {
|
|
481
|
+
throw new Error(`mobile runtime is not set up. Run: 11agents mobile setup`)
|
|
482
|
+
}
|
|
483
|
+
return python
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function normalizePythonCommand(command) {
|
|
487
|
+
return COMMAND_ALIASES.get(command) || command
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function buildRunContext(rawArgs, deps = {}) {
|
|
491
|
+
const { flags } = parseRawArgs(rawArgs)
|
|
492
|
+
const env = deps.env || process.env
|
|
493
|
+
const home = mobileHome(flags, env)
|
|
494
|
+
const taskId = taskIdFrom(flags, env)
|
|
495
|
+
const runDir = path.join(home, 'runs', taskId)
|
|
496
|
+
return {
|
|
497
|
+
flags,
|
|
498
|
+
home,
|
|
499
|
+
taskId,
|
|
500
|
+
runDir,
|
|
501
|
+
logPath: path.join(runDir, 'log'),
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function dispatchPython(rawArgs, deps = {}) {
|
|
506
|
+
const command = normalizePythonCommand(rawArgs[0] || '')
|
|
507
|
+
if (!PYTHON_COMMANDS.has(command)) throw new Error(`unsupported mobile command: ${rawArgs[0] || ''}`)
|
|
508
|
+
const context = buildRunContext(rawArgs, deps)
|
|
509
|
+
const python = await ensureRunnable(context.home, deps)
|
|
510
|
+
const childArgs = ['-m', 'device_control.cli', command, ...stripWrapperFlags(rawArgs.slice(1))]
|
|
511
|
+
const env = pythonEnv(context.home, deps.env || process.env)
|
|
512
|
+
env.ELEVENAGENTS_MOBILE_RUN_DIR = context.runDir
|
|
513
|
+
env.ELEVENAGENTS_TASK_ID = context.taskId
|
|
514
|
+
const result = await runAndLog({
|
|
515
|
+
command: python,
|
|
516
|
+
args: childArgs,
|
|
517
|
+
cwd: context.home,
|
|
518
|
+
env,
|
|
519
|
+
runDir: context.runDir,
|
|
520
|
+
log: context.logPath,
|
|
521
|
+
deps,
|
|
522
|
+
})
|
|
523
|
+
const parsed = parseJsonResult(result.stdout)
|
|
524
|
+
await writeFile(path.join(context.runDir, 'result.json'), JSON.stringify({
|
|
525
|
+
status: result.code === 0 ? 'ok' : 'failed',
|
|
526
|
+
exit_code: result.code,
|
|
527
|
+
parsed,
|
|
528
|
+
}, null, 2))
|
|
529
|
+
if (result.stdout) process.stdout.write(result.stdout)
|
|
530
|
+
if (result.stderr) process.stderr.write(result.stderr)
|
|
531
|
+
if (result.code !== 0) process.exitCode = result.code
|
|
532
|
+
return { ...result, task_id: context.taskId, run_dir: context.runDir, parsed }
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function dispatchScript(rawArgs, deps = {}) {
|
|
536
|
+
const command = rawArgs[0] || ''
|
|
537
|
+
const scriptName = SCRIPT_COMMANDS.get(command)
|
|
538
|
+
if (!scriptName) throw new Error(`unsupported mobile command: ${command}`)
|
|
539
|
+
const context = buildRunContext(rawArgs, deps)
|
|
540
|
+
const python = await ensureRunnable(context.home, deps)
|
|
541
|
+
const scriptPath = path.join(MOBILE_SCRIPTS_ROOT, scriptName)
|
|
542
|
+
const env = pythonEnv(context.home, deps.env || process.env)
|
|
543
|
+
env.ELEVENAGENTS_MOBILE_RUN_DIR = context.runDir
|
|
544
|
+
env.ELEVENAGENTS_TASK_ID = context.taskId
|
|
545
|
+
const result = await runAndLog({
|
|
546
|
+
command: python,
|
|
547
|
+
args: [scriptPath, ...stripWrapperFlags(rawArgs.slice(1))],
|
|
548
|
+
cwd: context.home,
|
|
549
|
+
env,
|
|
550
|
+
runDir: context.runDir,
|
|
551
|
+
log: context.logPath,
|
|
552
|
+
deps,
|
|
553
|
+
})
|
|
554
|
+
await writeFile(path.join(context.runDir, 'result.json'), JSON.stringify({
|
|
555
|
+
status: result.code === 0 ? 'ok' : 'failed',
|
|
556
|
+
exit_code: result.code,
|
|
557
|
+
}, null, 2))
|
|
558
|
+
if (result.stdout) process.stdout.write(result.stdout)
|
|
559
|
+
if (result.stderr) process.stderr.write(result.stderr)
|
|
560
|
+
if (result.code !== 0) process.exitCode = result.code
|
|
561
|
+
return { ...result, task_id: context.taskId, run_dir: context.runDir }
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function parseJsonResult(text) {
|
|
565
|
+
const rows = []
|
|
566
|
+
for (const line of String(text || '').split('\n')) {
|
|
567
|
+
const trimmed = line.trim()
|
|
568
|
+
if (!trimmed) continue
|
|
569
|
+
try {
|
|
570
|
+
rows.push(JSON.parse(trimmed))
|
|
571
|
+
} catch {}
|
|
572
|
+
}
|
|
573
|
+
return rows.length ? rows : null
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function removeFlags(args, names) {
|
|
577
|
+
const out = []
|
|
578
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
579
|
+
const arg = args[i]
|
|
580
|
+
if (!arg.startsWith('--')) {
|
|
581
|
+
out.push(arg)
|
|
582
|
+
continue
|
|
583
|
+
}
|
|
584
|
+
const raw = arg.slice(2)
|
|
585
|
+
const eq = raw.indexOf('=')
|
|
586
|
+
const key = eq >= 0 ? raw.slice(0, eq) : raw
|
|
587
|
+
if (!names.has(key)) {
|
|
588
|
+
out.push(arg)
|
|
589
|
+
continue
|
|
590
|
+
}
|
|
591
|
+
if (eq < 0) {
|
|
592
|
+
const next = args[i + 1]
|
|
593
|
+
if (next && !next.startsWith('--')) i += 1
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return out
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function dataCollectCommand(args) {
|
|
600
|
+
const { flags } = parseRawArgs(args)
|
|
601
|
+
const platform = String(flag(flags, 'platform') || '').toLowerCase()
|
|
602
|
+
const scope = String(flag(flags, 'scope') || flag(flags, 'kind') || '').toLowerCase()
|
|
603
|
+
const pass = removeFlags(args, new Set(['platform', 'scope', 'kind']))
|
|
604
|
+
if (!platform) throw new Error('mobile data collect requires --platform')
|
|
605
|
+
if (platform === 'tiktok' || platform === 'tk') {
|
|
606
|
+
if (scope === 'account') return ['collect-tiktok-account-metrics-adb', ...pass]
|
|
607
|
+
if (scope === 'video' || flags['video-order'] || flags.device) return ['collect-tiktok-video-metrics-adb', ...pass]
|
|
608
|
+
return ['collect-metrics', '--platform', 'tiktok', ...pass]
|
|
609
|
+
}
|
|
610
|
+
if (platform === 'reddit' || platform === 'instagram') {
|
|
611
|
+
return ['collect-metrics', '--platform', platform, ...pass]
|
|
612
|
+
}
|
|
613
|
+
if (platform === 'xiaohongshu' && flags['copy-link']) {
|
|
614
|
+
return ['copy-xiaohongshu-link', ...removeFlags(pass, new Set(['copy-link']))]
|
|
615
|
+
}
|
|
616
|
+
return null
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function mobileDataCollect(args, deps = {}) {
|
|
620
|
+
const command = dataCollectCommand(args)
|
|
621
|
+
if (command) return dispatchPython(command, deps)
|
|
622
|
+
const { flags } = parseRawArgs(args)
|
|
623
|
+
const context = buildRunContext(args, deps)
|
|
624
|
+
await scaffoldMobileHome(context.home)
|
|
625
|
+
await mkdir(context.runDir, { recursive: true })
|
|
626
|
+
const platform = String(flag(flags, 'platform') || '').toLowerCase()
|
|
627
|
+
const recordId = String(flag(flags, 'record-id') || '')
|
|
628
|
+
const result = {
|
|
629
|
+
platform,
|
|
630
|
+
record_id: recordId,
|
|
631
|
+
status: 'missing_access',
|
|
632
|
+
reason: `No automatic mobile data collector is bundled for platform=${platform}. Use platform API/export or manual values, then append with 11agents mobile add-metrics.`,
|
|
633
|
+
run_dir: context.runDir,
|
|
634
|
+
}
|
|
635
|
+
await writeFile(path.join(context.runDir, 'result.json'), JSON.stringify(result, null, 2))
|
|
636
|
+
await appendLog(context.logPath, `${JSON.stringify(result)}\n`)
|
|
637
|
+
;(deps.log || console.log)(JSON.stringify(result, null, 2))
|
|
638
|
+
return { code: 0, stdout: JSON.stringify(result), stderr: '', parsed: [result] }
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export async function runMobile(argv = [], deps = {}) {
|
|
642
|
+
const { flags } = parseRawArgs(argv)
|
|
643
|
+
const { positional } = parseRawArgs(argv)
|
|
644
|
+
const [command, subcommand] = positional
|
|
645
|
+
if (!argv.length || command === 'help') {
|
|
646
|
+
usage()
|
|
647
|
+
return null
|
|
648
|
+
}
|
|
649
|
+
if (command === 'setup') return mobileSetup(flags, deps)
|
|
650
|
+
if (command === 'doctor') return mobileDoctor(flags, deps)
|
|
651
|
+
if (command === 'migrate') return mobileMigrate(flags, deps)
|
|
652
|
+
if (command === 'skills') return mobileSkills(restAndFlagsAfter(argv, 1), deps)
|
|
653
|
+
if (command === 'data' && subcommand === 'collect') return mobileDataCollect(restAndFlagsAfter(argv, 2), deps)
|
|
654
|
+
if (command === 'groupctl') {
|
|
655
|
+
if (!subcommand) throw new Error('mobile groupctl requires a command')
|
|
656
|
+
return dispatchPython([subcommand, ...restAndFlagsAfter(argv, 2)], deps)
|
|
657
|
+
}
|
|
658
|
+
if (SCRIPT_COMMANDS.has(command)) return dispatchScript([command, ...restAndFlagsAfter(argv, 1)], deps)
|
|
659
|
+
return dispatchPython([command, ...restAndFlagsAfter(argv, 1)], deps)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function restAndFlagsAfter(argv, positionalCount) {
|
|
663
|
+
const out = []
|
|
664
|
+
let seen = 0
|
|
665
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
666
|
+
const arg = argv[i]
|
|
667
|
+
if (arg.startsWith('--')) {
|
|
668
|
+
out.push(arg)
|
|
669
|
+
const raw = arg.slice(2)
|
|
670
|
+
if (!raw.includes('=')) {
|
|
671
|
+
const next = argv[i + 1]
|
|
672
|
+
if (next && !next.startsWith('--')) {
|
|
673
|
+
out.push(next)
|
|
674
|
+
i += 1
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
continue
|
|
678
|
+
}
|
|
679
|
+
if (seen < positionalCount) {
|
|
680
|
+
seen += 1
|
|
681
|
+
continue
|
|
682
|
+
}
|
|
683
|
+
out.push(arg)
|
|
684
|
+
}
|
|
685
|
+
return out
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export const mobileInternals = {
|
|
689
|
+
dataCollectCommand,
|
|
690
|
+
mobileHome,
|
|
691
|
+
parseRawArgs,
|
|
692
|
+
stripWrapperFlags,
|
|
693
|
+
taskIdFrom,
|
|
694
|
+
venvPython,
|
|
695
|
+
}
|