Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/dependency-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ on:
- "docs/pnpm-workspace.yaml"
- "scripts/dependency-audit-fixtures/**"
push:
tags: ['v[0-9]*']
branches: [main]
paths:
- ".github/workflows/dependency-audit.yml"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ name: Deploy VitePress site to Pages
on:
# 在 docs 相关变更推送时运行构建(任意分支)
push:
branches: ['**']
tags: ['v[0-9]*']
paths:
- 'docs/**'
- '.github/workflows/deploy.yml'
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/publish-yuxi-cli.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Publish yuxi-cli

on:
release:
types: [published]
workflow_dispatch:

permissions:
Expand Down
54 changes: 10 additions & 44 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ name: Ruff Format Check

on:
push:
tags: ['v[0-9]*']
branches:
- main
paths:
- 'backend/**/*.py'
- 'backend/pyproject.toml'
- 'backend/uv.lock'
- 'Makefile'
- '.github/workflows/ruff.yml'
pull_request:
Expand All @@ -22,6 +24,7 @@ on:
paths:
- 'backend/**/*.py'
- 'backend/pyproject.toml'
- 'backend/uv.lock'
- 'Makefile'
- '.github/workflows/ruff.yml'

Expand Down Expand Up @@ -53,50 +56,13 @@ jobs:
with:
python-version: '3.12'

# 4. 安装 ruff 依赖(根据项目配置,ruff 在 dev 依赖组中)
- name: Install dependencies
- name: Install locked Ruff without runtime dependencies
run: |
uv sync --group dev
RUFF_VERSION=$(python -c 'import tomllib; from pathlib import Path; print(next(p["version"] for p in tomllib.loads(Path("uv.lock").read_text())["package"] if p["name"] == "ruff"))')
uv tool install --no-config --default-index https://pypi.org/simple "ruff==$RUFF_VERSION"

# 5. 运行 Ruff 格式检查(与项目的 make lint 命令一致)
- name: Run Ruff format check
- name: Check lint, formatting and imports
run: |
set +e # 不立即退出失败

echo "Running ruff check (uv run python -m ruff check package)..."
uv run python -m ruff check package
ruff_check_result=$?

echo "Running ruff format diff check (uv run python -m ruff format package --diff)..."
uv run python -m ruff format package --diff
ruff_format_result=$?

echo "Running import sorting check (uv run python -m ruff check --select I package)..."
uv run python -m ruff check --select I package
ruff_import_result=$?

# 检查是否有任何错误
if [ $ruff_check_result -eq 0 ] && [ $ruff_format_result -eq 0 ] && [ $ruff_import_result -eq 0 ]; then
echo "✅ Ruff format check passed"
exit 0
fi

echo "❌ Ruff format check failed"
echo "::error::Ruff format check found issues that must be addressed"

# 汇总错误信息
echo "## Ruff Format Issues Summary" > $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The following formatting issues were found:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

# 运行 ruff check 以获取详细错误信息
echo "### Detailed Issues:" >> $GITHUB_STEP_SUMMARY
uv run python -m ruff check package --output-format=github >> $GITHUB_STEP_SUMMARY 2>&1 || true

echo "" >> $GITHUB_STEP_SUMMARY
echo "To fix these issues locally, run:" >> $GITHUB_STEP_SUMMARY
echo '```bash' >> $GITHUB_STEP_SUMMARY
echo "make format" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
ruff check package
ruff format package --check
ruff check --select I package
7 changes: 4 additions & 3 deletions .github/workflows/system-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
- 'scripts/migrate-storage.sh'
- '.github/workflows/system-tests.yml'
push:
tags: ['v[0-9]*']
branches: [main]
paths:
- '.env.template'
Expand Down Expand Up @@ -154,17 +155,17 @@ jobs:
- name: Verify AgentRun lease ownership
run: docker compose exec -T api uv run --no-sync --no-dev pytest test/integration/services/test_agent_run_lease.py -q
- name: Verify Run result causality
run: docker compose exec -T api uv run --no-sync --no-dev pytest test/integration/api/test_agent_run_result_causality.py -q
run: docker compose exec -T -e TEST_USERNAME="$E2E_USERNAME" -e TEST_PASSWORD="$E2E_PASSWORD" api uv run --no-sync --no-dev pytest test/integration/api/test_agent_run_result_causality.py -q
- name: Verify Message audit HTTP contract
timeout-minutes: 3
run: docker compose exec -T -e TEST_USERNAME="$E2E_USERNAME" -e TEST_PASSWORD="$E2E_PASSWORD" api uv run --no-sync --no-dev pytest test/integration/api/test_chat_router.py::test_thread_message_audits_return_persisted_facts_without_leaking_into_history -q --setup-show -o faulthandler_timeout=60
- name: Verify deterministic Agent assembled path
run: docker compose exec -T -e E2E_USERNAME -e E2E_PASSWORD api uv run --no-sync --no-dev pytest test/e2e/test_deterministic_agent_path_e2e.py -q
- name: Verify identity transaction and replayable secret publication
run: docker compose exec -T api uv run --no-sync --no-dev pytest test/integration/services/test_identity_admin_service.py test/integration/services/test_api_key_schema_migration.py test/integration/services/test_api_key_user_lifecycle.py test/integration/api/test_apikey_router.py -q
run: docker compose exec -T -e TEST_USERNAME="$E2E_USERNAME" -e TEST_PASSWORD="$E2E_PASSWORD" api uv run --no-sync --no-dev pytest test/integration/services/test_identity_admin_service.py test/integration/services/test_api_key_schema_migration.py test/integration/services/test_api_key_user_lifecycle.py test/integration/api/test_apikey_router.py -q
- name: Verify destructive storage migration and Skill authorization
run: |
docker compose exec -T api uv run --no-sync --no-dev pytest \
docker compose exec -T -e TEST_USERNAME="$E2E_USERNAME" -e TEST_PASSWORD="$E2E_PASSWORD" api uv run --no-sync --no-dev pytest \
test/integration/services/test_workdir_user_workspace.py \
test/integration/services/test_user_skill_projection.py \
test/integration/api/test_skill_artifact_authorization.py -q
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ on:
- 'scripts/migrate-storage.sh'
- '.github/workflows/test.yml'
push:
tags: ['v[0-9]*']
branches:
- main
paths:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/trust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name: Engineering Trust Contracts
on:
pull_request:
push:
tags: ['v[0-9]*']
branches: [main]
workflow_dispatch:

Expand All @@ -27,3 +28,5 @@ jobs:
run: python3 -m unittest scripts.test_dependency_update_policy
- name: Verify release version synchronization
run: python3 -m unittest scripts.test_bump_version
- name: Verify release workflow boundaries
run: python3 -m unittest scripts.test_release_workflows
1 change: 1 addition & 0 deletions .github/workflows/web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- 'web/**'
- '.github/workflows/web.yml'
push:
tags: ['v[0-9]*']
branches: [main]
paths:
- 'web/**'
Expand Down
3 changes: 2 additions & 1 deletion backend/package/yuxi/knowledge/chunking/ragflow_like/nlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,10 @@ def hard_split_by_token_limit(text: str, chunk_token_num: int, hard_limit_token_


def random_choices(arr: list[str], k: int) -> list[str]:
"""无放回抽样,短文档保留全部段落供语言与标题识别。"""
if not arr:
return []
return random.choices(arr, k=min(len(arr), k))
return random.sample(arr, k=min(len(arr), k))


def is_english(texts: str | list[str]) -> bool:
Expand Down
11 changes: 8 additions & 3 deletions backend/test/unit/plugins/test_ragflow_like_chunking.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

import pytest
import random

import pytest
from yuxi.knowledge.chunking.ragflow_like import nlp
from yuxi.knowledge.chunking.ragflow_like.dispatcher import chunk_markdown
from yuxi.knowledge.chunking.ragflow_like.nlp import bullets_category, count_tokens
from yuxi.knowledge.chunking.ragflow_like.utils.semantic_utils import split_mixed_sentences, split_sentences_chinese
from yuxi.knowledge.chunking.ragflow_like.presets import (
CHUNK_ENGINE_VERSION,
CHUNK_PRESET_IDS,
Expand All @@ -14,6 +15,7 @@
map_to_internal_parser_id,
resolve_chunk_processing_params,
)
from yuxi.knowledge.chunking.ragflow_like.utils.semantic_utils import split_mixed_sentences, split_sentences_chinese
from yuxi.knowledge.utils.kb_utils import resolve_processing_params, sanitize_processing_params


Expand Down Expand Up @@ -101,7 +103,10 @@ def test_chunk_records_include_reserved_position_fields() -> None:
assert "start_char_pos" in chunks[1]


def test_book_chunking_hierarchical_merge() -> None:
@pytest.mark.parametrize("seed", [0, 94])
def test_book_chunking_hierarchical_merge(monkeypatch, seed) -> None:
"""抽样不得遗漏短文档标题,导致同一文档随机丢失分节。"""
monkeypatch.setattr(nlp, "random", random.Random(seed))
content = """
第一章 总则
第一节 适用范围
Expand Down
4 changes: 3 additions & 1 deletion docs/develop-guides/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## v0.7.3(待发布)

以下变更以 `v0.7.2` 正式 tag 为基线。
以下变更以 `v0.7.2` 正式 tag 为基线。0.7.3 新增定时智能体任务和主动上下文压缩,完善推理与工具结果展示、执行审计及后台任务恢复,并优化并发与解析依赖。CLI 沿用 0.1.4,本次应用更新不发布新的 CLI 包。

### 升级注意事项

Expand All @@ -26,6 +26,8 @@
- 修复普通 HTTP 环境创建 API Key 无响应([#998](https://github.com/xerrors/Yuxi/issues/998)),重试沿用同一幂等请求 ID;公网部署仍须配置 HTTPS。
- 修复知识库统计刷新命中过期缓存([#997](https://github.com/xerrors/Yuxi/issues/997))及带时区字段处理错误([#988](https://github.com/xerrors/Yuxi/issues/988))。
- 修复共享智能体编辑时误删不可见的既有资源选择;运行时仍只使用当前用户有权访问的资源。新建托管 Project 使用可读目录名,既有 UUID 目录保持有效。
- 修复子智能体禁用工具后仍可调用后端执行的问题;禁用状态在执行端拒绝工具访问([#1001](https://github.com/xerrors/Yuxi/pull/1001))。
- 修复书籍分块偶发遗漏短文档标题、将不同节错误合并的问题;标题识别使用无放回抽样,短文档保留全部段落。

### 运行与维护

Expand Down
11 changes: 11 additions & 0 deletions docs/develop-guides/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ PR 正文按创建方式选择模板:

模板复杂度不同,不改变非平凡或高风险变更的工程证据要求。来自 Fork 的 PR 默认无法读取主仓库 Secrets;不要通过修改工作流、打印环境变量或扩大权限绕过这一限制。如果验证必须依赖受保护凭证,应在 PR 中说明并由维护者执行对应检查。

### 候选版本与正式发布

维护者在发布前定稿版本号、changelog 和升级说明。功能更新以最近的正式 tag 为基线,例如 0.7.3 使用 `v0.7.2..HEAD`;候选版本之间的修复归并到对应功能,不单独替代完整发布说明。

1. 在已审查的提交上创建候选 tag,例如 `v0.7.3-rc.1`,显式推送该 tag。需要对外试用时创建 GitHub Release 并标记 Pre-release。
2. 在 Actions 核对该 tag 的工程契约、后端单测、Ruff、Web、运行链路、依赖审计和文档构建结果;tag 检查覆盖完整范围。真实 provider 与生产备份恢复演练按[测试规范](./testing-guidelines.md)和[升级指南](../advanced/deployment.md)补充,记录未验证范围。
3. 修复产生新提交时创建下一个候选 tag;已推送的候选 tag 保留原指向。
4. 最终候选通过后,在同一提交新增正式 tag,并发布正式 Release。Release 正文保留相对上一正式版本的完整功能更新及升级注意事项。应用 tag 触发检查,文档站只在 main 分支推送时部署。

CLI 使用 `packages/yuxi-cli/pyproject.toml` 中的独立版本。需要发布 CLI 时先提交包版本和锁文件更新,再对明确的提交或 tag 手动运行 [Publish yuxi-cli](https://github.com/xerrors/Yuxi/actions/workflows/publish-yuxi-cli.yml);应用 Release 不触发 PyPI 上传。CLI 版本未变时无需重复发布,上传失败须检查版本与 PyPI 状态。

## 7. 文档维护

代码、配置、API、状态、权限或命令变化时,同一 PR 更新对应的 owning page:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 书籍分块采用无放回抽样

状态:implemented
类型:bug-fix
Owner:backend/package/yuxi/knowledge/chunking/ragflow_like/nlp.py

## 问题

书籍解析器抽样判断语言和标题类型。有放回抽样即使在文档短于样本上限时也会重复抽中正文并漏掉全部标题,同一份短文档可能随机从按节分块退化为整块合并。发布检查的全量 unit 暴露此问题。

## 决策

抽样使用 `random.sample`,样本数量仍取文档段落数与原有上限的较小值。短文档的全部段落均参与语言与标题识别,长文档继续采用有界随机样本。两个现有调用者均只统计语言或标题类别,不依赖样本顺序。

## 替代方案

- 只在测试中固定幸运随机种子:保留用户文档偶发错误。
- 全文扫描:改变长文档的采样成本与现有启发式范围。

## 后果

短文档的标题不会因重复抽样丢失;长文档仍可能因采样未覆盖稀疏标题而选择普通分块,此修复不承诺长文档分块完全确定。

## 验证

`test_book_chunking_hierarchical_merge` 使用独立 `random.Random` 实例,保留两节精确内容 oracle。种子 94 在原实现稳定得到一个错误合并块,修复后与种子 0 均返回两个带章标题的节。运行 `docker compose exec -T api uv run --no-sync pytest test/unit/plugins/test_ragflow_like_chunking.py -q`,由后端 unit workflow 阻断回归。此修复沿用抽样接口、上限与分块返回契约,可直接记录为完整局部 bug-fix。
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 候选发布验证与 CLI 独立发布

状态:implemented
类型:process
Owner:.github/workflows/system-tests.yml

## 问题

应用发布与 CLI 包版本独立,但应用 Release 触发未升版 CLI 的重复 PyPI 上传。候选 tag 缺少完整 CI 触发,运行链路中的部分 HTTP 测试缺少认证变量而跳过,Ruff 安装整个后端依赖使检查受无关镜像下载失败影响。

## 决策

`.github/workflows` 中的发布门禁监听 `v[0-9]*` tag,覆盖工程契约、后端、前端、运行链路、依赖审计和文档构建;分支继续使用原有路径过滤。文档部署保留 main 分支限制。Ruff workflow 从后端锁文件读取工具版本并通过官方 PyPI 独立安装,检查 lint、格式和导入顺序。

运行链路 workflow 为 Run 结果归属、API Key 和 Skill 授权测试显式传入 CI 初始化的账号。工程契约 verifier 同步要求这些命令携带认证变量;恢复缺少账号的命令会使 gate 失败。

`publish-yuxi-cli.yml` 仅保留手动发布入口,CLI 包版本由其 pyproject 拥有。应用 Release 不触发 PyPI 写入。维护者以最近的正式 tag 为发布说明基线,候选验证成功后在相同提交新增正式 tag;操作由[贡献指南](../../contributing.md#候选版本与正式发布)维护。

## 替代方案

- 仅依赖 main 的路径过滤检查:无法直接查看候选 tag 的全部发布门禁结果。
- 应用发布时跳过 PyPI 已有文件:掩盖未升版 CLI 代码的误发布,继续耦合独立产品版本。
- Ruff 安装全部后端依赖:增加耗时和与格式检查无关的下载失败面。

## 后果

候选和正式 tag 都运行完整 CI,增加运行成本。CLI 发布需要维护者显式操作。真实 provider 与生产备份恢复仍由相应探针和部署演练验证,workflow 成功不证明这些未执行范围。

## 验证

- `python3 -m unittest scripts.test_release_workflows` 验证真实 workflow 的发布事件;删除任一门禁的 tag 触发或恢复 CLI 的 release 触发均被负向案例拒绝。该检查由 trust workflow 执行。
- `python3 scripts/verify_engineering_contracts.py` 与 `python3 -m unittest scripts.test_verify_engineering_contracts` 检查认证命令接线;删除账号或密码参数会使对应 HTTP 门禁报错。
- `docker compose exec -T api uv run --no-sync --no-dev pytest test/integration/api/test_agent_run_result_causality.py test/integration/api/test_apikey_router.py test/integration/api/test_skill_artifact_authorization.py -q -rs` 在真实服务上通过 18 项,零跳过;断言 Run 持久化归属、API Key 生命周期与撤权后的 Skill artifact 访问。
- Actionlint 校验工作流语法;Ruff、后端 unit 和文档构建分别由实际命令验证。远端 workflow 结果在 PR 中记录,不以本地配置检查替代 GitHub 执行。
77 changes: 77 additions & 0 deletions scripts/test_release_workflows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""发布事件与应用、CLI 发布边界的回归检查。"""

import re
import unittest
from pathlib import Path


WORKFLOWS = Path(__file__).resolve().parents[1] / ".github/workflows"


class ReleaseWorkflowTests(unittest.TestCase):
"""阻止候选检查缺失和应用 Release 误触发 CLI 上传。"""

def assert_release_events(self, workflows: dict[str, str]) -> None:
"""检查各发布门禁的真实事件声明。"""
for name in (
"trust",
"test",
"web",
"ruff",
"system-tests",
"dependency-audit",
"deploy",
):
events = workflows[name].split("\non:\n", 1)[1].split("\n\n", 1)[0]
push = events.split(" push:\n", 1)[1]
self.assertRegex(
push, r"(?m)^ tags: \['v\[0-9\]\*'\]$", f"{name}: 缺少版本 tag 触发"
)
cli_events = (
workflows["publish-yuxi-cli"].split("\non:\n", 1)[1].split("\n\n", 1)[0]
)
self.assertEqual(
re.findall(r"^ (\w+):", cli_events, re.MULTILINE),
["workflow_dispatch"],
"CLI 必须独立手动发布",
)

def test_repository_release_events(self) -> None:
"""当前配置覆盖候选与正式 tag,CLI 仅手动触发。"""
self.assert_release_events(
{path.stem: path.read_text() for path in WORKFLOWS.glob("*.yml")}
)

def test_missing_tag_trigger_is_rejected(self) -> None:
"""恢复仅监听分支的缺陷时对应门禁必须失败。"""
workflows = {path.stem: path.read_text() for path in WORKFLOWS.glob("*.yml")}
for name in (
"trust",
"test",
"web",
"ruff",
"system-tests",
"dependency-audit",
"deploy",
):
with (
self.subTest(workflow=name),
self.assertRaisesRegex(AssertionError, f"{name}: 缺少版本 tag 触发"),
):
self.assert_release_events(
workflows
| {name: workflows[name].replace(" tags: ['v[0-9]*']\n", "")}
)

def test_application_release_cannot_publish_cli(self) -> None:
"""恢复应用 Release 触发时禁止重复上传独立 CLI 包。"""
workflows = {path.stem: path.read_text() for path in WORKFLOWS.glob("*.yml")}
workflows["publish-yuxi-cli"] = workflows["publish-yuxi-cli"].replace(
"on:\n", "on:\n release:\n types: [published]\n", 1
)
with self.assertRaisesRegex(AssertionError, "CLI 必须独立手动发布"):
self.assert_release_events(workflows)


if __name__ == "__main__":
unittest.main()
Loading
Loading