-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Openclaw迁移功能(时间简史)初步方案 #6733
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Jacobinwwey
wants to merge
15
commits into
AstrBotDevs:master
Choose a base branch
from
Jacobinwwey:feat/openclaw-migration-time-brief-history
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Openclaw迁移功能(时间简史)初步方案 #6733
Changes from 12 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
a9170eb
feat(cli): add preliminary openclaw migration command
Jacobinwwey c24b07a
refactor(cli): harden openclaw migration tool and tests
Jacobinwwey b4eba8c
refactor(migrate): split toml utility and harden float/timestamp hand…
Jacobinwwey 3305a1f
refactor(migrate): improve determinism and robustness in openclaw import
Jacobinwwey 6c29d40
fix(cli): wrap openclaw sqlite read errors as click exceptions
Jacobinwwey c27096f
fix(migrate): harden sqlite key fallback and polish cli report
Jacobinwwey a6d49e8
refactor(migrate): streamline orchestration and toml normalization
Jacobinwwey 1f8a486
fix(migrate): normalize dry-run target and exclude nested output
Jacobinwwey 4dd7154
fix(migrate): tighten cli target semantics and add cmd regression test
Jacobinwwey 40bbb9f
refactor(toml): simplify quoting helpers with behavior parity
Jacobinwwey 7391ba4
refactor(toml): handle null sentinel inline without pre-pass
Jacobinwwey 9b136d5
refactor(migrate): modularize openclaw flow and harden dry-run scan
Jacobinwwey 6cd86de
fix(migrate): harden sqlite uri path handling and timeline markdown e…
Jacobinwwey 63a4aea
fix(migrate): build sqlite readonly uri via pathlib as_uri
Jacobinwwey 8f0a975
style(openclaw): apply ruff formatting
Jacobinwwey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| from .cmd_conf import conf | ||
| from .cmd_init import init | ||
| from .cmd_migrate import migrate | ||
| from .cmd_plug import plug | ||
| from .cmd_run import run | ||
|
|
||
| __all__ = ["conf", "init", "plug", "run"] | ||
| __all__ = ["conf", "init", "migrate", "plug", "run"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| import click | ||
|
|
||
| from ..utils import get_astrbot_root | ||
| from ..utils.openclaw_migrate import run_openclaw_migration | ||
|
|
||
|
|
||
| @click.group(name="migrate") | ||
| def migrate() -> None: | ||
| """Data migration utilities for external runtimes.""" | ||
|
|
||
|
|
||
| @migrate.command(name="openclaw") | ||
| @click.option( | ||
| "--source", | ||
| "source_path", | ||
| type=click.Path(path_type=Path, file_okay=False, resolve_path=True), | ||
| default=None, | ||
| help="Path to OpenClaw root directory (default: ~/.openclaw).", | ||
| ) | ||
| @click.option( | ||
| "--target", | ||
| "target_path", | ||
| type=click.Path(path_type=Path, file_okay=False, resolve_path=False), | ||
| default=None, | ||
| help=( | ||
| "Custom output directory. If omitted, writes to " | ||
| "data/migrations/openclaw/run-<timestamp>." | ||
| ), | ||
| ) | ||
| @click.option( | ||
| "--dry-run", | ||
| is_flag=True, | ||
| default=False, | ||
| help="Preview migration candidates without writing files.", | ||
| ) | ||
| def migrate_openclaw( | ||
| source_path: Path | None, | ||
| target_path: Path | None, | ||
| dry_run: bool, | ||
| ) -> None: | ||
| """Migrate OpenClaw workspace snapshots into AstrBot migration artifacts.""" | ||
|
|
||
| astrbot_root = get_astrbot_root() | ||
| source_root = source_path or (Path.home() / ".openclaw") | ||
|
|
||
| report = run_openclaw_migration( | ||
| source_root=source_root, | ||
| astrbot_root=astrbot_root, | ||
| dry_run=dry_run, | ||
| target_dir=target_path, | ||
| ) | ||
|
|
||
| click.echo("OpenClaw migration report:") | ||
| click.echo(f" Source root: {report.source_root}") | ||
| click.echo(f" Source workspace: {report.source_workspace}") | ||
| click.echo(f" Dry run: {report.dry_run}") | ||
| click.echo(f" Memory entries: {report.memory_entries_total}") | ||
| click.echo(f" - sqlite: {report.memory_entries_from_sqlite}") | ||
| click.echo(f" - markdown: {report.memory_entries_from_markdown}") | ||
| click.echo(f" Workspace files: {report.workspace_files_total}") | ||
| click.echo(f" Workspace size: {report.workspace_bytes_total} bytes") | ||
| click.echo(f" Config found: {report.config_found}") | ||
|
|
||
| if dry_run: | ||
| click.echo("") | ||
| click.echo("Dry-run mode: no files were written.") | ||
| if target_path is not None: | ||
| click.echo("Note: --target is ignored when --dry-run is enabled.") | ||
| click.echo("Run without --dry-run to perform migration.") | ||
| return | ||
|
|
||
| click.echo("") | ||
| click.echo(f"Migration output: {report.target_dir}") | ||
| click.echo(f" Copied files: {report.copied_workspace_files}") | ||
| click.echo(f" Imported memories: {report.copied_memory_entries}") | ||
| click.echo(f" Timeline written: {report.wrote_timeline}") | ||
| click.echo(f" Config TOML written: {report.wrote_config_toml}") | ||
| click.echo("Done.") | ||
|
|
||
|
|
||
| __all__ = ["migrate"] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import datetime as dt | ||
| import json | ||
| import os | ||
| import shutil | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| import click | ||
|
|
||
| from .openclaw_models import MemoryEntry | ||
| from .openclaw_toml import json_to_toml | ||
|
|
||
|
|
||
| def _is_within(path: Path, parent: Path) -> bool: | ||
| try: | ||
| path.resolve().relative_to(parent.resolve()) | ||
| return True | ||
| except (OSError, ValueError): | ||
| return False | ||
|
|
||
|
|
||
| def collect_workspace_files( | ||
| workspace_dir: Path, *, exclude_dir: Path | None = None | ||
| ) -> list[Path]: | ||
| files: list[Path] = [] | ||
| exclude_resolved = exclude_dir.resolve() if exclude_dir is not None else None | ||
|
|
||
| for root, dirnames, filenames in os.walk( | ||
| workspace_dir, topdown=True, followlinks=False | ||
| ): | ||
| root_path = Path(root) | ||
|
|
||
| pruned_dirs: list[str] = [] | ||
| for dirname in dirnames: | ||
| dir_path = root_path / dirname | ||
| if dir_path.is_symlink(): | ||
| continue | ||
| if exclude_resolved is not None and _is_within(dir_path, exclude_resolved): | ||
| continue | ||
| pruned_dirs.append(dirname) | ||
| dirnames[:] = pruned_dirs | ||
|
|
||
| for filename in filenames: | ||
| path = root_path / filename | ||
| if path.is_symlink() or not path.is_file(): | ||
| continue | ||
| if exclude_resolved is not None and _is_within(path, exclude_resolved): | ||
| continue | ||
| files.append(path) | ||
|
|
||
| return sorted(files) | ||
|
|
||
|
|
||
| def workspace_total_size(files: list[Path]) -> int: | ||
| total_bytes = 0 | ||
| for path in files: | ||
| try: | ||
| total_bytes += path.stat().st_size | ||
| except OSError: | ||
| # Best-effort accounting: files may disappear or become unreadable | ||
| # during migration scans. | ||
| continue | ||
| return total_bytes | ||
|
|
||
|
|
||
| def _write_jsonl(path: Path, entries: list[MemoryEntry]) -> None: | ||
| with path.open("w", encoding="utf-8") as fp: | ||
| for entry in entries: | ||
| fp.write( | ||
| json.dumps( | ||
| { | ||
| "key": entry.key, | ||
| "content": entry.content, | ||
| "category": entry.category, | ||
| "timestamp": entry.timestamp, | ||
| "source": entry.source, | ||
| }, | ||
| ensure_ascii=False, | ||
| ) | ||
| + "\n" | ||
| ) | ||
|
|
||
|
|
||
| def _write_timeline(path: Path, entries: list[MemoryEntry], source_root: Path) -> None: | ||
| ordered = sorted(entries, key=lambda e: (e.timestamp or "", e.order)) | ||
|
|
||
| lines: list[str] = [] | ||
| lines.append("# OpenClaw Migration - Time Brief History") | ||
| lines.append("") | ||
| lines.append("> 时间简史(初步方案):按时间汇总可迁移记忆条目。") | ||
| lines.append("") | ||
| lines.append(f"- Generated at: {dt.datetime.now(dt.timezone.utc).isoformat()}") | ||
| lines.append(f"- Source: `{source_root}`") | ||
| lines.append(f"- Total entries: {len(ordered)}") | ||
| lines.append("") | ||
| lines.append("## Timeline") | ||
| lines.append("") | ||
|
|
||
| for entry in ordered: | ||
| ts = entry.timestamp or "unknown" | ||
| snippet = entry.content.replace("\n", " ").strip() | ||
| if len(snippet) > 160: | ||
| snippet = snippet[:157] + "..." | ||
| lines.append(f"- [{ts}] ({entry.category}) `{entry.key}`: {snippet}") | ||
|
|
||
| lines.append("") | ||
|
sourcery-ai[bot] marked this conversation as resolved.
|
||
| path.write_text("\n".join(lines), encoding="utf-8") | ||
|
|
||
|
|
||
| def write_migration_artifacts( | ||
| *, | ||
| workspace_dir: Path, | ||
| workspace_files: list[Path], | ||
| resolved_target: Path, | ||
| source_root: Path, | ||
| memory_entries: list[MemoryEntry], | ||
| config_obj: dict[str, Any] | None, | ||
| config_json_path: Path | None, | ||
| ) -> tuple[int, int, bool, bool]: | ||
| workspace_target = resolved_target / "workspace" | ||
| workspace_target.mkdir(parents=True, exist_ok=True) | ||
|
|
||
| copied_workspace_files = 0 | ||
| for src_file in workspace_files: | ||
| rel_path = src_file.relative_to(workspace_dir) | ||
| dst_file = workspace_target / rel_path | ||
| dst_file.parent.mkdir(parents=True, exist_ok=True) | ||
| shutil.copy2(src_file, dst_file) | ||
| copied_workspace_files += 1 | ||
|
|
||
| copied_memory_entries = 0 | ||
| wrote_timeline = False | ||
| if memory_entries: | ||
| _write_jsonl(resolved_target / "memory_entries.jsonl", memory_entries) | ||
| copied_memory_entries = len(memory_entries) | ||
| _write_timeline( | ||
| resolved_target / "time_brief_history.md", | ||
| memory_entries, | ||
| source_root, | ||
| ) | ||
| wrote_timeline = True | ||
|
|
||
| wrote_config_toml = False | ||
| if config_obj is not None: | ||
| (resolved_target / "config.original.json").write_text( | ||
| json.dumps(config_obj, ensure_ascii=False, indent=2), | ||
| encoding="utf-8", | ||
| ) | ||
| try: | ||
| converted_toml = json_to_toml(config_obj) | ||
| except ValueError as exc: | ||
| source_hint = str(config_json_path) if config_json_path else "config JSON" | ||
| raise click.ClickException( | ||
| f"Failed to convert {source_hint} to TOML: {exc}" | ||
| ) from exc | ||
| (resolved_target / "config.migrated.toml").write_text( | ||
| converted_toml, | ||
| encoding="utf-8", | ||
| ) | ||
| wrote_config_toml = True | ||
|
|
||
| return copied_workspace_files, copied_memory_entries, wrote_timeline, wrote_config_toml | ||
|
|
||
|
|
||
| __all__ = ["collect_workspace_files", "workspace_total_size", "write_migration_artifacts"] | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.