@11agents/cli 0.1.34 → 0.1.36
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/mobile-runtime/python/src/device_control/publishers/tiktok_adb.py +12 -0
- package/mobile-runtime/python/src/device_control/publishers/tiktok_appium.py +1 -0
- package/mobile-runtime/python/src/device_control/publishers/xiaohongshu_adb.py +5 -1
- package/mobile-runtime/skills/android-publish-tiktok/SKILL.md +2 -0
- package/mobile-runtime/skills/android-publish-xiaohongshu/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/runtime.js +95 -3
|
@@ -142,6 +142,7 @@ class TikTokAdbPublisher:
|
|
|
142
142
|
status="published",
|
|
143
143
|
)
|
|
144
144
|
append_publish_record(self.records_path, record)
|
|
145
|
+
self._close_tiktok_best_effort(device, run_dir)
|
|
145
146
|
return TikTokAdbPublishResult(
|
|
146
147
|
device_id=device.device_id,
|
|
147
148
|
status=status,
|
|
@@ -242,6 +243,17 @@ class TikTokAdbPublisher:
|
|
|
242
243
|
self._ok(self.adb.launch_package(device.adb_serial, TIKTOK_PACKAGE), "launch TikTok")
|
|
243
244
|
time.sleep(5)
|
|
244
245
|
|
|
246
|
+
def _close_tiktok(self, device: Device) -> None:
|
|
247
|
+
self._ok(self.adb.force_stop(device.adb_serial, TIKTOK_PACKAGE), "close TikTok")
|
|
248
|
+
|
|
249
|
+
def _close_tiktok_best_effort(self, device: Device, run_dir: Path) -> None:
|
|
250
|
+
try:
|
|
251
|
+
self._close_tiktok(device)
|
|
252
|
+
time.sleep(1)
|
|
253
|
+
self.adb.screenshot(device.adb_serial, run_dir / "after-app-close.png")
|
|
254
|
+
except Exception:
|
|
255
|
+
pass
|
|
256
|
+
|
|
245
257
|
def _open_create(self, device: Device, run_dir: Path) -> None:
|
|
246
258
|
root = self._dump_and_check(device, run_dir, "before-create")
|
|
247
259
|
create_node = ui.find_node(
|
|
@@ -357,6 +357,7 @@ class XiaohongshuAdbPublisher:
|
|
|
357
357
|
status="published",
|
|
358
358
|
),
|
|
359
359
|
)
|
|
360
|
+
self._close_xiaohongshu_best_effort(device, run_dir)
|
|
360
361
|
return XiaohongshuAdbPublishResult(
|
|
361
362
|
device_id=device.device_id,
|
|
362
363
|
status=status,
|
|
@@ -531,9 +532,12 @@ class XiaohongshuAdbPublisher:
|
|
|
531
532
|
def _close_xiaohongshu(self, device: Device) -> None:
|
|
532
533
|
self._ok(self.adb.force_stop(device.adb_serial, self.package), "close Xiaohongshu")
|
|
533
534
|
|
|
534
|
-
def _close_xiaohongshu_best_effort(self, device: Device) -> None:
|
|
535
|
+
def _close_xiaohongshu_best_effort(self, device: Device, run_dir: Path | None = None) -> None:
|
|
535
536
|
try:
|
|
536
537
|
self._close_xiaohongshu(device)
|
|
538
|
+
if run_dir is not None:
|
|
539
|
+
time.sleep(self._wait("after_prompt_dismiss"))
|
|
540
|
+
self.adb.screenshot(device.adb_serial, run_dir / "after-app-close.png")
|
|
537
541
|
except Exception:
|
|
538
542
|
pass
|
|
539
543
|
|
|
@@ -34,6 +34,8 @@ ADB fallback when explicitly requested:
|
|
|
34
34
|
|
|
35
35
|
Use `--range`, `--parallel`, and `--max-concurrency N` only when the approved plan targets multiple devices. Keep concurrency at or below 5.
|
|
36
36
|
|
|
37
|
+
Successful live publish is only confirmed after the CLI taps the publish button and verifies TikTok publish completion. After writing the publish record, the CLI force-stops TikTok and captures `after-app-close.png` in the run artifacts.
|
|
38
|
+
|
|
37
39
|
## Output
|
|
38
40
|
|
|
39
41
|
Return publish status, record id, account/device, screenshot path, permalink or platform post id when available, duration, error action, and the data-collection handoff:
|
|
@@ -33,7 +33,7 @@ Structured package:
|
|
|
33
33
|
11agents mobile publish-xiaohongshu --device D03 --publish-package "/path/publish_package.json" --json --task-id TASK_123
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Successful live publish is only confirmed after the CLI taps the final publish button, waits without tapping while Xiaohongshu shows upload/progress state, closes and relaunches Xiaohongshu, then opens the "我" page, opens the note whose title matches the expected title or the first visible profile note as a verified fallback, and verifies the detail page against the expected title/body. The CLI recovers the note link by default for live publish; use `--no-copy-link-after-publish` only when link recovery is explicitly not wanted. If that evidence does not match, treat the result as `failed`; do not claim publish success from the publish button tap alone.
|
|
36
|
+
Successful live publish is only confirmed after the CLI taps the final publish button, waits without tapping while Xiaohongshu shows upload/progress state, closes and relaunches Xiaohongshu, then opens the "我" page, opens the note whose title matches the expected title or the first visible profile note as a verified fallback, and verifies the detail page against the expected title/body. The CLI recovers the note link by default for live publish; use `--no-copy-link-after-publish` only when link recovery is explicitly not wanted. After writing the publish record, the CLI force-stops Xiaohongshu and captures `after-app-close.png` in the run artifacts. If that evidence does not match, treat the result as `failed`; do not claim publish success from the publish button tap alone.
|
|
37
37
|
|
|
38
38
|
Link copy after publish or during the recovery skill:
|
|
39
39
|
|
package/package.json
CHANGED
package/src/commands/runtime.js
CHANGED
|
@@ -874,6 +874,99 @@ function normalizeSkillBundle(skill = {}) {
|
|
|
874
874
|
}
|
|
875
875
|
}
|
|
876
876
|
|
|
877
|
+
async function callProjectMcp(toolName, toolArgs, syncConfig, deps) {
|
|
878
|
+
const result = await deps.requestJson('/mcp', {
|
|
879
|
+
method: 'POST',
|
|
880
|
+
body: {
|
|
881
|
+
jsonrpc: '2.0',
|
|
882
|
+
id: 1,
|
|
883
|
+
method: 'tools/call',
|
|
884
|
+
params: { name: toolName, arguments: toolArgs },
|
|
885
|
+
},
|
|
886
|
+
config: syncConfig,
|
|
887
|
+
})
|
|
888
|
+
if (result.error) throw new Error(result.error.message || `MCP ${toolName} failed`)
|
|
889
|
+
const text = result.result?.content?.[0]?.text || '{}'
|
|
890
|
+
return JSON.parse(text)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function syncSkillsViaMcp({ task, workdir, flags, deps, projectToken, config }) {
|
|
894
|
+
const agentId = task.agent?.id
|
|
895
|
+
if (!agentId) return { changed: false, count: 0 }
|
|
896
|
+
|
|
897
|
+
const syncConfig = { ...config, token: projectToken }
|
|
898
|
+
const skillsDir = path.join(workdir, 'agents', slugify(agentNameForTask(task), 'agent'), 'skills')
|
|
899
|
+
|
|
900
|
+
const manifest = await callProjectMcp('skill_list', { agent_id: agentId }, syncConfig, deps)
|
|
901
|
+
const cloudSkills = Array.isArray(manifest.skills) ? manifest.skills : []
|
|
902
|
+
|
|
903
|
+
await mkdir(skillsDir, { recursive: true })
|
|
904
|
+
let changed = false
|
|
905
|
+
|
|
906
|
+
// Remove local skill dirs that no longer exist in the cloud
|
|
907
|
+
const cloudSkillSlugs = new Set(cloudSkills.map(s => slugify(s.name, 'skill')))
|
|
908
|
+
const localDirs = await readdir(skillsDir, { withFileTypes: true }).catch(() => [])
|
|
909
|
+
for (const entry of localDirs) {
|
|
910
|
+
if (entry.isDirectory() && !cloudSkillSlugs.has(entry.name)) {
|
|
911
|
+
await rm(path.join(skillsDir, entry.name), { recursive: true, force: true })
|
|
912
|
+
changed = true
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
for (const cloudSkill of cloudSkills) {
|
|
917
|
+
const skillSlug = slugify(cloudSkill.name, 'skill')
|
|
918
|
+
const skillDir = path.join(skillsDir, skillSlug)
|
|
919
|
+
const cloudFiles = Array.isArray(cloudSkill.files) ? cloudSkill.files : []
|
|
920
|
+
|
|
921
|
+
// Check for any missing local file
|
|
922
|
+
const presentFlags = await Promise.all(
|
|
923
|
+
cloudFiles.map(f => readFile(path.join(skillDir, assertSafeRelativePath(f.name))).then(() => true).catch(() => false))
|
|
924
|
+
)
|
|
925
|
+
const hasMissing = presentFlags.some(p => !p)
|
|
926
|
+
|
|
927
|
+
if (hasMissing) {
|
|
928
|
+
// Full re-fetch: delete skill dir and pull every file fresh
|
|
929
|
+
await rm(skillDir, { recursive: true, force: true })
|
|
930
|
+
await mkdir(skillDir, { recursive: true })
|
|
931
|
+
for (const cloudFile of cloudFiles) {
|
|
932
|
+
const fetched = await callProjectMcp('skill_file_get', {
|
|
933
|
+
agent_id: agentId,
|
|
934
|
+
skill_name: cloudSkill.name,
|
|
935
|
+
file_path: cloudFile.name,
|
|
936
|
+
}, syncConfig, deps)
|
|
937
|
+
const target = path.join(skillDir, assertSafeRelativePath(cloudFile.name))
|
|
938
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
939
|
+
const content = fetched.encoding === 'base64'
|
|
940
|
+
? Buffer.from(fetched.content || '', 'base64')
|
|
941
|
+
: String(fetched.content || '')
|
|
942
|
+
await writeFile(target, content)
|
|
943
|
+
}
|
|
944
|
+
changed = true
|
|
945
|
+
} else {
|
|
946
|
+
// All files present — diff by MD5
|
|
947
|
+
for (const cloudFile of cloudFiles) {
|
|
948
|
+
const localPath = path.join(skillDir, assertSafeRelativePath(cloudFile.name))
|
|
949
|
+
const localBuf = await readFile(localPath)
|
|
950
|
+
const localMd5 = createHash('md5').update(localBuf).digest('hex')
|
|
951
|
+
if (localMd5 !== cloudFile.md5) {
|
|
952
|
+
const fetched = await callProjectMcp('skill_file_get', {
|
|
953
|
+
agent_id: agentId,
|
|
954
|
+
skill_name: cloudSkill.name,
|
|
955
|
+
file_path: cloudFile.name,
|
|
956
|
+
}, syncConfig, deps)
|
|
957
|
+
const content = fetched.encoding === 'base64'
|
|
958
|
+
? Buffer.from(fetched.content || '', 'base64')
|
|
959
|
+
: String(fetched.content || '')
|
|
960
|
+
await writeFile(localPath, content)
|
|
961
|
+
changed = true
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
return { changed, count: cloudSkills.length, skills_dir: skillsDir }
|
|
968
|
+
}
|
|
969
|
+
|
|
877
970
|
async function materializeSkillsIfChanged({ task, workdir, flags, deps }) {
|
|
878
971
|
const skills = Array.isArray(task.agent?.skills) ? task.agent.skills.map(normalizeSkillBundle) : []
|
|
879
972
|
if (!skills.length) return { changed: false, count: 0 }
|
|
@@ -1016,11 +1109,10 @@ async function prepareRuntimeTask(task, flags, deps, config) {
|
|
|
1016
1109
|
await mkdir(agentMemoryDir, { recursive: true })
|
|
1017
1110
|
|
|
1018
1111
|
const database = await syncDatabaseIfNeeded({ task, workdir, config, flags, deps })
|
|
1019
|
-
const skills = await materializeSkillsIfChanged({ task, workdir, flags, deps })
|
|
1020
1112
|
|
|
1021
|
-
// Resolve
|
|
1022
|
-
// runtime agent can authenticate without needing credentials on disk.
|
|
1113
|
+
// Resolve project token before skill sync so MCP auth is available.
|
|
1023
1114
|
const projectToken = await projectSyncToken(projectTokenCandidatesForTask(task, flags), flags, deps, task.workspace?.swarm_token)
|
|
1115
|
+
const skills = await syncSkillsViaMcp({ task, workdir, flags, deps, projectToken, config })
|
|
1024
1116
|
const env = {
|
|
1025
1117
|
...process.env,
|
|
1026
1118
|
...agentEnvironment(task),
|