Skip to content

feat: add UMO config overrides#9147

Open
Soulter wants to merge 1 commit into
masterfrom
codex/umo-config-overrides
Open

feat: add UMO config overrides#9147
Soulter wants to merge 1 commit into
masterfrom
codex/umo-config-overrides

Conversation

@Soulter

@Soulter Soulter commented Jul 5, 2026

Copy link
Copy Markdown
Member

Summary

  • store UMO-level custom rules as core config override paths and apply effective configs per event
  • add path-based session config override APIs plus alias handling, keeping legacy rule APIs compatible
  • update the WebUI custom rule dialog to add/remove explicit overrides and reuse config renderers

Validation

  • uv run ruff format . && uv run ruff check .
  • cd dashboard && pnpm build

Note: dashboard build still reports existing warnings for mdi-platform/mdi-subset and vocechat.png runtime resolution.

@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Jul 5, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @Soulter, your pull request is larger than the review limit of 150000 diff characters

@dosubot dosubot Bot added area:core The bug / feature is about astrbot's core, backend area:webui The bug / feature is about webui(dashboard) of astrbot. labels Jul 5, 2026
)
)
except SessionManagementServiceError as exc:
return _service_error(exc)
except SessionManagementServiceError as exc:
return _service_error(exc)
except Exception as exc:
return _unexpected_error("获取会话配置覆盖项列表失败", exc)
)
)
except SessionManagementServiceError as exc:
return _service_error(exc)
except SessionManagementServiceError as exc:
return _service_error(exc)
except Exception as exc:
return _unexpected_error("更新会话配置覆盖项失败", exc)
)
)
except SessionManagementServiceError as exc:
return _service_error(exc)
except SessionManagementServiceError as exc:
return _service_error(exc)
except Exception as exc:
return _unexpected_error("删除会话配置覆盖项失败", exc)
await service.update_session_alias(payload.model_dump(exclude_unset=True))
)
except SessionManagementServiceError as exc:
return _service_error(exc)
except SessionManagementServiceError as exc:
return _service_error(exc)
except Exception as exc:
return _unexpected_error("更新会话备注失败", exc)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces the legacy session service and plugin managers with a unified configuration override system, allowing per-event and per-session configuration overrides for different Unified Message Origins (UMOs). It updates the pipeline stages to dynamically resolve the effective configuration and adds blacklist and plugin-disabling capabilities, along with corresponding database migrations and dashboard UI updates. The review feedback suggests several key improvements: raising AttributeError instead of returning None in EffectiveAstrBotConfig.__getattr__ to preserve standard Python behavior, removing a redundant call to get_current_effective_config(), refining payload normalization to prevent key collisions, filtering out None values in knowledge base retrieval to avoid downstream errors, and caching compiled regex patterns in the result decoration stage to improve performance.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +66 to +70
def __getattr__(self, item: str) -> Any:
try:
return self[item]
except KeyError:
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Returning None instead of raising AttributeError in __getattr__ is problematic because it breaks standard Python behaviors like hasattr(obj, name) (which will always return True) and getattr(obj, name, default) (which will return None instead of the default value). It is highly recommended to raise AttributeError when a key is not found.

Suggested change
def __getattr__(self, item: str) -> Any:
try:
return self[item]
except KeyError:
return None
def __getattr__(self, item: str) -> Any:
try:
return self[item]
except KeyError:
raise AttributeError(f"'EffectiveAstrBotConfig' object has no attribute '{item}'")

Comment on lines +174 to +178
if not umo:
effective_config = get_current_effective_config()
if effective_config:
return effective_config
return self.confs["default"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The second call to get_current_effective_config() is redundant because if umo is None, get_current_effective_config(None) was already called on line 170 and would have returned the effective config if it existed. We can simplify this block to directly return the default configuration.

Suggested change
if not umo:
effective_config = get_current_effective_config()
if effective_config:
return effective_config
return self.confs["default"]
if not umo:
return self.confs["default"]

Comment on lines +127 to +131
if not isinstance(payload, dict):
return {}
paths = payload.get("paths", payload)
if not isinstance(paths, dict):
return {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using payload.get("paths", payload) can cause collisions if there is a legitimate configuration path named "paths". A safer way to distinguish between the versioned wrapper dictionary and the raw paths dictionary is to check for the presence of the "version" key.

Suggested change
if not isinstance(payload, dict):
return {}
paths = payload.get("paths", payload)
if not isinstance(paths, dict):
return {}
if not isinstance(payload, dict):
return {}
paths = payload.get("paths") if "version" in payload else payload
if not isinstance(paths, dict):
return {}

kb_helper = await kb_mgr.get_kb(kb_ref)
if not kb_helper:
kb_helper = await kb_mgr.get_kb_by_name(kb_ref)
all_kbs.append(kb_helper)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If kb_helper is None (e.g., the knowledge base is not found by ID or name), appending it to all_kbs can cause downstream AttributeErrors in functions like check_all_kb or other retrieval steps. We should only append non-None helpers.

Suggested change
all_kbs.append(kb_helper)
if kb_helper:
all_kbs.append(kb_helper)

Comment on lines +104 to +115
if split_words:
escaped_words = sorted(
[re.escape(word) for word in split_words],
key=len,
reverse=True,
)
split_words_pattern = re.compile(
f"(.*?({'|'.join(escaped_words)})|.+$)",
re.DOTALL,
)
else:
split_words_pattern = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Compiling the split_words_pattern regex on every single message event process is inefficient because compiling regex patterns is CPU-intensive. We can cache the compiled pattern on the stage instance to avoid redundant compilation.

Suggested change
if split_words:
escaped_words = sorted(
[re.escape(word) for word in split_words],
key=len,
reverse=True,
)
split_words_pattern = re.compile(
f"(.*?({'|'.join(escaped_words)})|.+$)",
re.DOTALL,
)
else:
split_words_pattern = None
if not hasattr(self, "_split_words_pattern_cache"):
self._split_words_pattern_cache = {}
split_words_key = tuple(split_words) if split_words else None
if split_words_key not in self._split_words_pattern_cache:
if split_words:
escaped_words = sorted(
[re.escape(word) for word in split_words],
key=len,
reverse=True,
)
pattern = re.compile(
f"(.*?({'|'.join(escaped_words)})|.+$)",
re.DOTALL,
)
else:
pattern = None
self._split_words_pattern_cache[split_words_key] = pattern
split_words_pattern = self._split_words_pattern_cache[split_words_key]

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
astrbot-docs 31e7af8 Commit Preview URL

Branch Preview URL
Jul 05 2026, 06:52 AM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend area:webui The bug / feature is about webui(dashboard) of astrbot. size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants