Skip to content

Commit 043c744

Browse files
authored
Merge branch 'main' into fix/media-core-encoded-url-extension
2 parents 54f63f5 + e5259fa commit 043c744

317 files changed

Lines changed: 18332 additions & 2729 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/npm-telegram-beta-e2e.yml

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,117 @@ jobs:
247247
trap append_telegram_summary EXIT
248248
249249
if [[ -n "${PACKAGE_ARTIFACT_NAME// }" ]]; then
250-
mapfile -t package_tgzs < <(find .artifacts/telegram-package-under-test -type f -name "*.tgz" | sort)
251-
if [[ "${#package_tgzs[@]}" -ne 1 ]]; then
252-
echo "package artifact ${PACKAGE_ARTIFACT_NAME} must contain exactly one .tgz; found ${#package_tgzs[@]}" >&2
253-
exit 1
250+
package_dir=".artifacts/telegram-package-under-test"
251+
manifest="${package_dir}/preflight-manifest.json"
252+
if [[ -f "${manifest}" ]]; then
253+
package_tgz="$(
254+
node - "${manifest}" "${package_dir}" <<'NODE'
255+
const crypto = require("node:crypto");
256+
const childProcess = require("node:child_process");
257+
const fs = require("node:fs");
258+
const path = require("node:path");
259+
const [manifestPath, packageDir] = process.argv.slice(2);
260+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
261+
const entries = [
262+
{
263+
packageName: "openclaw",
264+
packageVersion: manifest.packageVersion,
265+
tarballName: manifest.tarballName,
266+
tarballSha256: manifest.tarballSha256,
267+
},
268+
...(Array.isArray(manifest.dependencyTarballs) ? manifest.dependencyTarballs : []),
269+
];
270+
const packageNames = new Set();
271+
const tarballNames = new Set();
272+
for (const entry of entries) {
273+
if (
274+
!entry.packageName ||
275+
!entry.packageVersion ||
276+
!entry.tarballName ||
277+
!entry.tarballSha256 ||
278+
entry.tarballName !== path.basename(entry.tarballName)
279+
) {
280+
throw new Error("package artifact manifest contains invalid tarball metadata");
281+
}
282+
if (packageNames.has(entry.packageName) || tarballNames.has(entry.tarballName)) {
283+
throw new Error("package artifact manifest contains duplicate package metadata");
284+
}
285+
packageNames.add(entry.packageName);
286+
tarballNames.add(entry.tarballName);
287+
const tarballPath = path.join(packageDir, entry.tarballName);
288+
if (!fs.existsSync(tarballPath)) {
289+
throw new Error(`package artifact is missing ${entry.tarballName}`);
290+
}
291+
const digest = crypto.createHash("sha256").update(fs.readFileSync(tarballPath)).digest("hex");
292+
if (digest !== entry.tarballSha256) {
293+
throw new Error(`package artifact digest mismatch for ${entry.packageName}`);
294+
}
295+
const packageJson = JSON.parse(
296+
childProcess.execFileSync("tar", ["-xOf", tarballPath, "package/package.json"], {
297+
encoding: "utf8",
298+
}),
299+
);
300+
if (packageJson.name !== entry.packageName || packageJson.version !== entry.packageVersion) {
301+
throw new Error(`package artifact metadata mismatch for ${entry.packageName}`);
302+
}
303+
}
304+
const artifactTarballs = fs
305+
.readdirSync(packageDir, { withFileTypes: true })
306+
.filter((entry) => entry.isFile() && entry.name.endsWith(".tgz"))
307+
.map((entry) => entry.name);
308+
if (
309+
artifactTarballs.length !== tarballNames.size ||
310+
artifactTarballs.some((name) => !tarballNames.has(name))
311+
) {
312+
throw new Error("package artifact tarball set does not match preflight manifest");
313+
}
314+
process.stdout.write(path.join(packageDir, manifest.tarballName));
315+
NODE
316+
)"
317+
export OPENCLAW_NPM_TELEGRAM_PACKAGE_DIR="${package_dir}"
318+
else
319+
mapfile -t package_tgzs < <(find "${package_dir}" -type f -name "*.tgz" | sort)
320+
if [[ "${#package_tgzs[@]}" -ne 1 ]]; then
321+
echo "package artifact ${PACKAGE_ARTIFACT_NAME} without a preflight manifest must contain exactly one tgz; found ${#package_tgzs[@]}" >&2
322+
exit 1
323+
fi
324+
package_tgz="${package_tgzs[0]}"
325+
candidate_manifest="${package_dir}/package-candidate.json"
326+
node - "${package_tgz}" "${candidate_manifest}" <<'NODE'
327+
const crypto = require("node:crypto");
328+
const childProcess = require("node:child_process");
329+
const fs = require("node:fs");
330+
const path = require("node:path");
331+
const [tarballPath, manifestPath] = process.argv.slice(2);
332+
const packageJson = JSON.parse(
333+
childProcess.execFileSync("tar", ["-xOf", tarballPath, "package/package.json"], {
334+
encoding: "utf8",
335+
}),
336+
);
337+
if (packageJson.name !== "openclaw" || typeof packageJson.version !== "string") {
338+
throw new Error("package artifact tgz is not a versioned openclaw package");
339+
}
340+
if (fs.existsSync(manifestPath)) {
341+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
342+
const expectedTarballName = path.basename(manifest.tarball || "");
343+
if (
344+
manifest.name !== "openclaw" ||
345+
manifest.version !== packageJson.version ||
346+
expectedTarballName !== path.basename(tarballPath) ||
347+
typeof manifest.sha256 !== "string"
348+
) {
349+
throw new Error("package candidate manifest does not match the OpenClaw tarball");
350+
}
351+
const digest = crypto.createHash("sha256").update(fs.readFileSync(tarballPath)).digest("hex");
352+
if (digest !== manifest.sha256) {
353+
throw new Error("package candidate digest mismatch");
354+
}
355+
}
356+
NODE
254357
fi
255-
export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgzs[0]}"
358+
export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgz}"
256359
if [[ -z "${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL// }" ]]; then
257-
export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$(basename "${package_tgzs[0]}")"
360+
export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$(basename "${package_tgz}")"
258361
fi
259362
elif [[ -z "${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL// }" ]]; then
260363
export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="${OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC}"

.github/workflows/openclaw-npm-release.yml

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,17 +396,33 @@ jobs:
396396
fi
397397
TARBALL_NAME="$PACK_NAME"
398398
TARBALL_SHA256="$(sha256sum "$PACK_PATH" | awk '{print $1}')"
399+
mapfile -t AI_TARBALLS < <(find "$AI_RUNTIME_TARBALL_DIR" -maxdepth 1 -type f -name 'openclaw-ai-*.tgz' -print | sort)
400+
if [[ "${#AI_TARBALLS[@]}" -ne 1 ]]; then
401+
echo "Expected exactly one packed @openclaw/ai tarball, found ${#AI_TARBALLS[@]}." >&2
402+
exit 1
403+
fi
404+
AI_TARBALL_PATH="${AI_TARBALLS[0]}"
405+
AI_TARBALL_NAME="$(basename "$AI_TARBALL_PATH")"
406+
AI_TARBALL_SHA256="$(sha256sum "$AI_TARBALL_PATH" | awk '{print $1}')"
407+
AI_PACKAGE_VERSION="$(
408+
tar -xOf "$AI_TARBALL_PATH" package/package.json |
409+
node -e 'let raw = ""; process.stdin.on("data", (chunk) => (raw += chunk)); process.stdin.on("end", () => process.stdout.write(JSON.parse(raw).version));'
410+
)"
411+
if [[ "$AI_PACKAGE_VERSION" != "$PACKAGE_VERSION" ]]; then
412+
echo "Prepared @openclaw/ai version ${AI_PACKAGE_VERSION} does not match openclaw ${PACKAGE_VERSION}." >&2
413+
exit 1
414+
fi
399415
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-npm-preflight"
400416
rm -rf "$ARTIFACT_DIR"
401417
mkdir -p "$ARTIFACT_DIR"
402418
cp "$PACK_PATH" "$ARTIFACT_DIR/"
403-
cp "$AI_RUNTIME_TARBALL_DIR"/*.tgz "$ARTIFACT_DIR/"
419+
cp "$AI_TARBALL_PATH" "$ARTIFACT_DIR/"
404420
cp "$AI_RUNTIME_TARBALL_DIR/SHA256SUMS" "$ARTIFACT_DIR/ai-runtime-SHA256SUMS"
405421
cp -R "$DEPENDENCY_EVIDENCE_DIR" "$ARTIFACT_DIR/dependency-evidence"
406422
printf '%s\n' "$RELEASE_TAG" > "$ARTIFACT_DIR/release-tag.txt"
407423
printf '%s\n' "$RELEASE_SHA" > "$ARTIFACT_DIR/release-sha.txt"
408424
printf '%s\n' "$RELEASE_NPM_DIST_TAG" > "$ARTIFACT_DIR/release-npm-dist-tag.txt"
409-
ARTIFACT_DIR="$ARTIFACT_DIR" RELEASE_TAG="$RELEASE_TAG" RELEASE_SHA="$RELEASE_SHA" RELEASE_NPM_DIST_TAG="$RELEASE_NPM_DIST_TAG" PACKAGE_VERSION="$PACKAGE_VERSION" TARBALL_NAME="$TARBALL_NAME" TARBALL_SHA256="$TARBALL_SHA256" node <<'NODE'
425+
ARTIFACT_DIR="$ARTIFACT_DIR" RELEASE_TAG="$RELEASE_TAG" RELEASE_SHA="$RELEASE_SHA" RELEASE_NPM_DIST_TAG="$RELEASE_NPM_DIST_TAG" PACKAGE_VERSION="$PACKAGE_VERSION" TARBALL_NAME="$TARBALL_NAME" TARBALL_SHA256="$TARBALL_SHA256" AI_PACKAGE_VERSION="$AI_PACKAGE_VERSION" AI_TARBALL_NAME="$AI_TARBALL_NAME" AI_TARBALL_SHA256="$AI_TARBALL_SHA256" node <<'NODE'
410426
const fs = require("node:fs");
411427
const path = require("node:path");
412428
const manifest = {
@@ -418,6 +434,14 @@ jobs:
418434
packageVersion: process.env.PACKAGE_VERSION,
419435
tarballName: process.env.TARBALL_NAME,
420436
tarballSha256: process.env.TARBALL_SHA256,
437+
dependencyTarballs: [
438+
{
439+
packageName: "@openclaw/ai",
440+
packageVersion: process.env.AI_PACKAGE_VERSION,
441+
tarballName: process.env.AI_TARBALL_NAME,
442+
tarballSha256: process.env.AI_TARBALL_SHA256,
443+
},
444+
],
421445
dependencyEvidenceDir: "dependency-evidence",
422446
dependencyEvidenceManifest: "dependency-evidence/dependency-evidence-manifest.json",
423447
};

apps/.i18n/native-source.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,31 +131,31 @@
131131
},
132132
{
133133
"kind": "ui-named-argument",
134-
"line": 199,
134+
"line": 247,
135135
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainActivity.kt",
136136
"source": "OPENCLAW",
137137
"surface": "android",
138138
"id": "native.android.ff4c22906eac1e9e"
139139
},
140140
{
141141
"kind": "ui-state-text",
142-
"line": 138,
142+
"line": 143,
143143
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt",
144144
"source": "Searching…",
145145
"surface": "android",
146146
"id": "native.android.dec357dfc9b8c0a5"
147147
},
148148
{
149149
"kind": "ui-state-text",
150-
"line": 238,
150+
"line": 243,
151151
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt",
152152
"source": "Mic off",
153153
"surface": "android",
154154
"id": "native.android.567482ea1118eb13"
155155
},
156156
{
157157
"kind": "ui-state-text",
158-
"line": 248,
158+
"line": 253,
159159
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt",
160160
"source": "Off",
161161
"surface": "android",
@@ -379,7 +379,7 @@
379379
},
380380
{
381381
"kind": "ui-state-text",
382-
"line": 86,
382+
"line": 106,
383383
"path": "apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayDiscovery.kt",
384384
"source": "Searching…",
385385
"surface": "android",

apps/android/app/src/main/java/ai/openclaw/app/MainActivity.kt

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,15 @@ class MainActivity : ComponentActivity() {
4242
private val viewModel: MainViewModel by viewModels()
4343
private lateinit var permissionRequester: PermissionRequester
4444
private var initializedViewModel: MainViewModel? = null
45-
private var didAttachRuntimeUi = false
46-
private var didStartNodeService = false
4745
private var didStartViewModelCollectors = false
4846
private var foreground = false
49-
private var pendingIntent: Intent? = null
47+
private val pendingIntentRouter = MainActivityPendingIntentRouter()
48+
private val runtimeUiStarter = MainActivityRuntimeUiStarter()
5049
private var screenshotScene: AndroidScreenshotScene? = null
5150

5251
override fun onCreate(savedInstanceState: Bundle?) {
5352
super.onCreate(savedInstanceState)
54-
pendingIntent = intent
53+
pendingIntentRouter.setInitialIntent(intent)
5554
WindowCompat.setDecorFitsSystemWindows(window, false)
5655
permissionRequester = PermissionRequester(this)
5756
if (BuildConfig.DEBUG) {
@@ -111,8 +110,9 @@ class MainActivity : ComponentActivity() {
111110
override fun onNewIntent(intent: android.content.Intent) {
112111
super.onNewIntent(intent)
113112
setIntent(intent)
114-
pendingIntent = intent
115-
initializedViewModel?.let { handleAssistantIntent(viewModel = it, intent = intent) }
113+
pendingIntentRouter.onNewIntent(intent) { routedIntent ->
114+
initializedViewModel?.let { handleAssistantIntent(viewModel = it, intent = routedIntent) }
115+
}
116116
}
117117

118118
/**
@@ -123,9 +123,8 @@ class MainActivity : ComponentActivity() {
123123
initializedViewModel = readyViewModel
124124
readyViewModel.setForeground(foreground)
125125
startViewModelCollectors(readyViewModel)
126-
pendingIntent?.let { initialIntent ->
126+
pendingIntentRouter.activate { initialIntent ->
127127
handleAssistantIntent(viewModel = readyViewModel, intent = initialIntent)
128-
pendingIntent = null
129128
}
130129
}
131130

@@ -151,18 +150,17 @@ class MainActivity : ComponentActivity() {
151150
lifecycleScope.launch {
152151
repeatOnLifecycle(Lifecycle.State.STARTED) {
153152
readyViewModel.runtimeInitialized.collect { ready ->
154-
if (!ready || didAttachRuntimeUi) return@collect
155-
if (screenshotScene != null) {
156-
didAttachRuntimeUi = true
157-
return@collect
158-
}
159-
// Runtime UI helpers need an Activity owner, so attach once after NodeRuntime is ready.
160-
readyViewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
161-
didAttachRuntimeUi = true
162-
if (!didStartNodeService) {
163-
NodeForegroundService.start(this@MainActivity)
164-
didStartNodeService = true
165-
}
153+
runtimeUiStarter.onRuntimeInitialized(
154+
ready = ready,
155+
startRuntimeUi = screenshotScene == null,
156+
attachRuntimeUi = {
157+
// Runtime UI helpers need an Activity owner, so attach once after NodeRuntime is ready.
158+
readyViewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
159+
},
160+
startNodeService = {
161+
NodeForegroundService.start(this@MainActivity)
162+
},
163+
)
166164
}
167165
}
168166
}
@@ -184,6 +182,56 @@ class MainActivity : ComponentActivity() {
184182
}
185183
}
186184

185+
/** Holds launch intents until ViewModel activation, then routes every later intent immediately. */
186+
internal class MainActivityPendingIntentRouter {
187+
private var activated = false
188+
private var pendingIntent: Intent? = null
189+
190+
fun setInitialIntent(intent: Intent?) {
191+
if (!activated) pendingIntent = intent
192+
}
193+
194+
fun onNewIntent(
195+
intent: Intent,
196+
routeIntent: (Intent) -> Unit,
197+
) {
198+
if (activated) {
199+
routeIntent(intent)
200+
return
201+
}
202+
pendingIntent = intent
203+
}
204+
205+
fun activate(routeIntent: (Intent) -> Unit): Boolean {
206+
if (activated) return false
207+
activated = true
208+
pendingIntent?.let(routeIntent)
209+
pendingIntent = null
210+
return true
211+
}
212+
}
213+
214+
/** Preserves one-shot runtime UI startup while allowing screenshot fixtures to skip side effects. */
215+
internal class MainActivityRuntimeUiStarter {
216+
private var completed = false
217+
218+
fun onRuntimeInitialized(
219+
ready: Boolean,
220+
startRuntimeUi: Boolean,
221+
attachRuntimeUi: () -> Unit,
222+
startNodeService: () -> Unit,
223+
) {
224+
if (!ready || completed) return
225+
if (!startRuntimeUi) {
226+
completed = true
227+
return
228+
}
229+
attachRuntimeUi()
230+
completed = true
231+
startNodeService()
232+
}
233+
}
234+
187235
@Composable
188236
private fun StartupSurface() {
189237
Surface(

apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ data class ChatDraft(
4646
val placement: ChatDraftPlacement,
4747
)
4848

49+
internal fun shouldStartRuntimeOnForeground(
50+
foreground: Boolean,
51+
onboardingCompleted: Boolean,
52+
): Boolean = foreground && onboardingCompleted
53+
4954
/**
5055
* UI-facing bridge that exposes NodeRuntime and preference state as Compose-friendly StateFlows.
5156
*/
@@ -298,7 +303,12 @@ class MainViewModel(
298303
*/
299304
fun setForeground(value: Boolean) {
300305
foreground = value
301-
if (value && prefs.onboardingCompleted.value) {
306+
if (
307+
shouldStartRuntimeOnForeground(
308+
foreground = value,
309+
onboardingCompleted = prefs.onboardingCompleted.value,
310+
)
311+
) {
302312
queueRuntimeStartup()
303313
}
304314
runtimeRef.value?.setForeground(value)

0 commit comments

Comments
 (0)