Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8ba63e087 | ||
|
|
309ed277fe |
@@ -28,6 +28,8 @@ background scheduler
|
|||||||
|
|
||||||
- `server.py` is the stable command/import facade. Runtime composition lives in
|
- `server.py` is the stable command/import facade. Runtime composition lives in
|
||||||
`backend/application.py` and `backend/bootstrap/`.
|
`backend/application.py` and `backend/bootstrap/`.
|
||||||
|
- `backend/bootstrap/` owns process configuration, dependency construction, startup, and
|
||||||
|
shared input/display-format contracts. It does not own feature behavior.
|
||||||
- `backend/http/` owns common authentication, request IDs, responses, static delivery, and
|
- `backend/http/` owns common authentication, request IDs, responses, static delivery, and
|
||||||
error normalization. Feature-specific transport handlers live beside their feature.
|
error normalization. Feature-specific transport handlers live beside their feature.
|
||||||
Exact POST endpoints that only delegate to one of those handlers use the explicit maps in
|
Exact POST endpoints that only delegate to one of those handlers use the explicit maps in
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ def normalize_date(value: str) -> str:
|
|||||||
return parsed.strftime("%Y%m%d")
|
return parsed.strftime("%Y%m%d")
|
||||||
|
|
||||||
|
|
||||||
|
def display_compact_date(value: str) -> str:
|
||||||
|
return f"{value[:4]}-{value[4:6]}-{value[6:8]}" if len(value) == 8 else value
|
||||||
|
|
||||||
|
|
||||||
def validate_stock_code(value: str) -> str:
|
def validate_stock_code(value: str) -> str:
|
||||||
code = value.strip()
|
code = value.strip()
|
||||||
if not re.fullmatch(r"\d{6}", code):
|
if not re.fullmatch(r"\d{6}", code):
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from datetime import datetime, time as dt_time, timedelta
|
|||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from backend.bootstrap.config import display_compact_date as _display_date
|
||||||
from backend.data.numbers import finite_number as _number
|
from backend.data.numbers import finite_number as _number
|
||||||
from backend.features.sentiment.engine import apply_sentiment_to_dashboard
|
from backend.features.sentiment.engine import apply_sentiment_to_dashboard
|
||||||
|
|
||||||
@@ -2007,10 +2008,6 @@ def _display_time(value: Any) -> str:
|
|||||||
return f"{raw[:2]}:{raw[2:4]}:{raw[4:6]}"
|
return f"{raw[:2]}:{raw[2:4]}:{raw[4:6]}"
|
||||||
|
|
||||||
|
|
||||||
def _display_date(value: str) -> str:
|
|
||||||
return f"{value[:4]}-{value[4:6]}-{value[6:8]}" if len(value) == 8 else value
|
|
||||||
|
|
||||||
|
|
||||||
def _realtime_market_status(current_time: dt_time) -> str:
|
def _realtime_market_status(current_time: dt_time) -> str:
|
||||||
if current_time < dt_time(9, 25):
|
if current_time < dt_time(9, 25):
|
||||||
return "pre_open"
|
return "pre_open"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from collections import defaultdict
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from backend.bootstrap.config import display_compact_date as _display_date
|
||||||
from backend.data.numbers import finite_number as _number
|
from backend.data.numbers import finite_number as _number
|
||||||
from backend.data.providers.tushare_client import TushareClient, TushareError
|
from backend.data.providers.tushare_client import TushareClient, TushareError
|
||||||
from backend.features.screener.strategies import ADVANCED_CURATED_STRATEGIES
|
from backend.features.screener.strategies import ADVANCED_CURATED_STRATEGIES
|
||||||
@@ -2200,7 +2201,3 @@ def _regime_reason(regime: str) -> str:
|
|||||||
"divergence": "指数或核心仍强,但广度、封板质量开始分化。",
|
"divergence": "指数或核心仍强,但广度、封板质量开始分化。",
|
||||||
"retreat": "情绪指标继续走弱,应提高筛选门槛并接受无候选结果。",
|
"retreat": "情绪指标继续走弱,应提高筛选门槛并接受无候选结果。",
|
||||||
}.get(regime, "市场阶段待确认。")
|
}.get(regime, "市场阶段待确认。")
|
||||||
|
|
||||||
|
|
||||||
def _display_date(value: str) -> str:
|
|
||||||
return f"{value[:4]}-{value[4:6]}-{value[6:8]}" if len(value) == 8 else value
|
|
||||||
|
|||||||
@@ -220,6 +220,25 @@
|
|||||||
"runtime_role": "index observation fallback"
|
"runtime_role": "index observation fallback"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"provider_construction": [
|
||||||
|
{
|
||||||
|
"client": "TushareClient",
|
||||||
|
"owner": "backend/data/providers/tushare.py",
|
||||||
|
"compatibility_fallback": "backend/features/market/service.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "IfindHttpClient",
|
||||||
|
"owner": "backend/data/gateway.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "MarketChartClient",
|
||||||
|
"owner": "backend/data/gateway.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "WebRealtimeAggregator",
|
||||||
|
"owner": "backend/data/gateway.py"
|
||||||
|
}
|
||||||
|
],
|
||||||
"numeric_normalization": [
|
"numeric_normalization": [
|
||||||
{
|
{
|
||||||
"function": "finite_number",
|
"function": "finite_number",
|
||||||
@@ -230,6 +249,12 @@
|
|||||||
"path": "backend/data/numbers.py"
|
"path": "backend/data/numbers.py"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"date_formatting": [
|
||||||
|
{
|
||||||
|
"function": "display_compact_date",
|
||||||
|
"path": "backend/bootstrap/config.py"
|
||||||
|
}
|
||||||
|
],
|
||||||
"llm_entrypoints": [
|
"llm_entrypoints": [
|
||||||
{
|
{
|
||||||
"function": "stream_with_mentor",
|
"function": "stream_with_mentor",
|
||||||
@@ -289,13 +314,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/features/screener/engine.py",
|
"path": "backend/features/screener/engine.py",
|
||||||
"bytes": 108434,
|
"bytes": 108387,
|
||||||
"lines": 2206
|
"lines": 2203
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/data/providers/tushare_client.py",
|
"path": "backend/data/providers/tushare_client.py",
|
||||||
"bytes": 94171,
|
"bytes": 94124,
|
||||||
"lines": 2168
|
"lines": 2165
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "frontend/app.js",
|
"path": "frontend/app.js",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from backend.data import (
|
from backend.data import (
|
||||||
DataPolicyError,
|
DataPolicyError,
|
||||||
@@ -45,8 +47,6 @@ class DataGatewayTests(unittest.TestCase):
|
|||||||
self.assertIs(gateway.chart_data.ifind, gateway.ifind)
|
self.assertIs(gateway.chart_data.ifind, gateway.ifind)
|
||||||
|
|
||||||
def test_server_has_no_direct_runtime_tushare_construction(self) -> None:
|
def test_server_has_no_direct_runtime_tushare_construction(self) -> None:
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
source = (
|
source = (
|
||||||
Path(__file__).resolve().parents[1]
|
Path(__file__).resolve().parents[1]
|
||||||
/ "backend"
|
/ "backend"
|
||||||
@@ -57,6 +57,33 @@ class DataGatewayTests(unittest.TestCase):
|
|||||||
self.assertEqual(source.count("TushareClient(self.token)"), 1)
|
self.assertEqual(source.count("TushareClient(self.token)"), 1)
|
||||||
self.assertIn("return gateway.tushare()", source)
|
self.assertIn("return gateway.tushare()", source)
|
||||||
|
|
||||||
|
def test_provider_construction_has_unique_declared_owners(self) -> None:
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
owners = {
|
||||||
|
"EastmoneyChartClient": {"backend/data/gateway.py"},
|
||||||
|
"IfindHttpClient": {"backend/data/gateway.py"},
|
||||||
|
"IfindProvider": {"backend/data/gateway.py"},
|
||||||
|
"MarketChartClient": {"backend/data/gateway.py"},
|
||||||
|
"TushareClient": {"backend/features/market/service.py"},
|
||||||
|
"TushareProvider": {"backend/data/gateway.py"},
|
||||||
|
"WebRealtimeAggregator": {"backend/data/gateway.py"},
|
||||||
|
}
|
||||||
|
found = {name: set() for name in owners}
|
||||||
|
for path in (root / "backend").rglob("*.py"):
|
||||||
|
relative = path.relative_to(root).as_posix()
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, ast.Call):
|
||||||
|
continue
|
||||||
|
name = getattr(node.func, "id", None) or getattr(node.func, "attr", None)
|
||||||
|
if name in found:
|
||||||
|
found[name].add(relative)
|
||||||
|
self.assertEqual(found, owners)
|
||||||
|
provider_source = (root / "backend/data/providers/tushare.py").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
self.assertIn("client_factory: Callable[[str], TushareClient] = TushareClient", provider_source)
|
||||||
|
|
||||||
def test_quality_gate_accepts_matching_daily_evidence(self) -> None:
|
def test_quality_gate_accepts_matching_daily_evidence(self) -> None:
|
||||||
timezone = market_timezone()
|
timezone = market_timezone()
|
||||||
now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
|
now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ class MarketSliceSourceEquivalenceTests(unittest.TestCase):
|
|||||||
self.assertEqual(sha256(ORIGINAL_ROOT / original), sha256(APP_ROOT / migrated))
|
self.assertEqual(sha256(ORIGINAL_ROOT / original), sha256(APP_ROOT / migrated))
|
||||||
original_tushare = top_level_definitions(ORIGINAL_ROOT / "tushare_client.py")
|
original_tushare = top_level_definitions(ORIGINAL_ROOT / "tushare_client.py")
|
||||||
original_tushare.pop("_number")
|
original_tushare.pop("_number")
|
||||||
|
original_tushare.pop("_display_date")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
original_tushare,
|
original_tushare,
|
||||||
top_level_definitions(APP_ROOT / "backend/data/providers/tushare_client.py"),
|
top_level_definitions(APP_ROOT / "backend/data/providers/tushare_client.py"),
|
||||||
@@ -155,6 +156,13 @@ class MarketSliceSourceEquivalenceTests(unittest.TestCase):
|
|||||||
function_contract(APP_ROOT / "backend/data/numbers.py", "finite_number"),
|
function_contract(APP_ROOT / "backend/data/numbers.py", "finite_number"),
|
||||||
)
|
)
|
||||||
self.assertIs(canonical_tushare._number, finite_number)
|
self.assertIs(canonical_tushare._number, finite_number)
|
||||||
|
self.assertEqual(
|
||||||
|
function_contract(ORIGINAL_ROOT / "tushare_client.py", "_display_date"),
|
||||||
|
function_contract(
|
||||||
|
APP_ROOT / "backend/bootstrap/config.py", "display_compact_date"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertIs(canonical_tushare._display_date, bootstrap_config.display_compact_date)
|
||||||
original_charts = top_level_definitions(ORIGINAL_ROOT / "chart_data_provider.py")
|
original_charts = top_level_definitions(ORIGINAL_ROOT / "chart_data_provider.py")
|
||||||
original_charts.pop("_stock_market_code")
|
original_charts.pop("_stock_market_code")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import advanced_strategies
|
|||||||
import llm_strategy
|
import llm_strategy
|
||||||
import screener
|
import screener
|
||||||
import strategy_tracking
|
import strategy_tracking
|
||||||
|
from backend.bootstrap import config as bootstrap_config
|
||||||
from backend.data.numbers import finite_number
|
from backend.data.numbers import finite_number
|
||||||
from backend.features.screener import compiler, engine, strategies, tracking
|
from backend.features.screener import compiler, engine, strategies, tracking
|
||||||
from backend.features.screener import service as screener_service
|
from backend.features.screener import service as screener_service
|
||||||
@@ -151,12 +152,12 @@ class ScreenerSliceSourceEquivalenceTests(unittest.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
module_contract(
|
module_contract(
|
||||||
ORIGINAL_ROOT / "screener.py",
|
ORIGINAL_ROOT / "screener.py",
|
||||||
excluded_definitions={"_number"},
|
excluded_definitions={"_display_date", "_number"},
|
||||||
exclude_imports=True,
|
exclude_imports=True,
|
||||||
),
|
),
|
||||||
module_contract(
|
module_contract(
|
||||||
APP_ROOT / "backend" / "features" / "screener" / "engine.py",
|
APP_ROOT / "backend" / "features" / "screener" / "engine.py",
|
||||||
excluded_definitions={"_number"},
|
excluded_definitions={"_display_date", "_number"},
|
||||||
exclude_imports=True,
|
exclude_imports=True,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -165,6 +166,13 @@ class ScreenerSliceSourceEquivalenceTests(unittest.TestCase):
|
|||||||
function_contract(APP_ROOT / "backend/data/numbers.py", "finite_number"),
|
function_contract(APP_ROOT / "backend/data/numbers.py", "finite_number"),
|
||||||
)
|
)
|
||||||
self.assertIs(engine._number, finite_number)
|
self.assertIs(engine._number, finite_number)
|
||||||
|
self.assertEqual(
|
||||||
|
function_contract(ORIGINAL_ROOT / "screener.py", "_display_date"),
|
||||||
|
function_contract(
|
||||||
|
APP_ROOT / "backend/bootstrap/config.py", "display_compact_date"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertIs(engine._display_date, bootstrap_config.display_compact_date)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
class_methods(
|
class_methods(
|
||||||
ORIGINAL_ROOT / "backend" / "features" / "screener" / "tracking.py",
|
ORIGINAL_ROOT / "backend" / "features" / "screener" / "tracking.py",
|
||||||
|
|||||||
@@ -155,10 +155,19 @@ def build() -> dict[str, Any]:
|
|||||||
{"provider": "eastmoney", "path": "backend/data/realtime.py", "runtime_role": "isolated realtime observation"},
|
{"provider": "eastmoney", "path": "backend/data/realtime.py", "runtime_role": "isolated realtime observation"},
|
||||||
{"provider": "tencent", "path": "backend/data/realtime.py", "runtime_role": "index observation fallback"},
|
{"provider": "tencent", "path": "backend/data/realtime.py", "runtime_role": "index observation fallback"},
|
||||||
],
|
],
|
||||||
|
"provider_construction": [
|
||||||
|
{"client": "TushareClient", "owner": "backend/data/providers/tushare.py", "compatibility_fallback": "backend/features/market/service.py"},
|
||||||
|
{"client": "IfindHttpClient", "owner": "backend/data/gateway.py"},
|
||||||
|
{"client": "MarketChartClient", "owner": "backend/data/gateway.py"},
|
||||||
|
{"client": "WebRealtimeAggregator", "owner": "backend/data/gateway.py"},
|
||||||
|
],
|
||||||
"numeric_normalization": [
|
"numeric_normalization": [
|
||||||
{"function": "finite_number", "path": "backend/data/numbers.py"},
|
{"function": "finite_number", "path": "backend/data/numbers.py"},
|
||||||
{"function": "non_nan_number", "path": "backend/data/numbers.py"},
|
{"function": "non_nan_number", "path": "backend/data/numbers.py"},
|
||||||
],
|
],
|
||||||
|
"date_formatting": [
|
||||||
|
{"function": "display_compact_date", "path": "backend/bootstrap/config.py"},
|
||||||
|
],
|
||||||
"llm_entrypoints": [
|
"llm_entrypoints": [
|
||||||
{"function": "stream_with_mentor", "path": "backend/features/mentor/agent.py"},
|
{"function": "stream_with_mentor", "path": "backend/features/mentor/agent.py"},
|
||||||
{"function": "interpret_heaven", "path": "backend/features/heaven/agent.py"},
|
{"function": "interpret_heaven", "path": "backend/features/heaven/agent.py"},
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
| CR-03 | 股票市场后缀转换 | Tushare业务与iFinD图表各保留一份完全相同的沪深京代码转换函数 | 图表复用`bootstrap/config.py::tushare_code`,只保留一份函数体 | 已完成 |
|
| CR-03 | 股票市场后缀转换 | Tushare业务与iFinD图表各保留一份完全相同的沪深京代码转换函数 | 图表复用`bootstrap/config.py::tushare_code`,只保留一份函数体 | 已完成 |
|
||||||
| CR-04 | 数值归一化策略 | 四个业务模块分别保留两组完全相同的数值转换函数体 | 由`backend/data/numbers.py`集中拥有两种既有语义,消费者保留原局部别名 | 已完成 |
|
| CR-04 | 数值归一化策略 | 四个业务模块分别保留两组完全相同的数值转换函数体 | 由`backend/data/numbers.py`集中拥有两种既有语义,消费者保留原局部别名 | 已完成 |
|
||||||
| CR-05 | 根级兼容入口 | 正式后端仍有五处通过迁移兼容模块反向导入规范实现 | 正式代码改用规范路径;兼容入口只服务原公开导入契约 | 已完成 |
|
| CR-05 | 根级兼容入口 | 正式后端仍有五处通过迁移兼容模块反向导入规范实现 | 正式代码改用规范路径;兼容入口只服务原公开导入契约 | 已完成 |
|
||||||
|
| CR-06 | 紧凑日期显示 | Tushare与选股引擎各保留一份完全相同的`YYYYMMDD`显示转换 | 由`bootstrap/config.py`拥有唯一格式策略,消费者保留原局部别名 | 已完成 |
|
||||||
|
| CR-07 | 数据Provider组装 | 核查网关、容器和业务服务是否重复创建外部数据客户端 | 固化唯一创建位置及兼容例外,不改数据源语义 | 已完成 |
|
||||||
|
|
||||||
## CR-01验收口径
|
## CR-01验收口径
|
||||||
|
|
||||||
@@ -139,6 +141,50 @@
|
|||||||
本批基线为`xiaobai-reduction-04-numeric-normalization-20260801`;检查点为
|
本批基线为`xiaobai-reduction-04-numeric-normalization-20260801`;检查点为
|
||||||
`xiaobai-reduction-05-compatibility-boundaries-20260801`。
|
`xiaobai-reduction-05-compatibility-boundaries-20260801`。
|
||||||
|
|
||||||
|
## CR-06验收口径
|
||||||
|
|
||||||
|
- 只合并参数、函数体和异常行为完全相同的日期/文本转换;名称相似但空值、未来日期、错误文案或
|
||||||
|
输入格式不同的函数不得合并。
|
||||||
|
- Tushare与选股引擎继续暴露局部`_display_date`名称,并分别指向唯一共享实现。
|
||||||
|
- 原版两个`_display_date`函数必须分别与共享实现AST相等,所有原调用结果保持不变。
|
||||||
|
- 市场洞察的日期显示函数会清理连字符并容忍空值,语义不同,必须继续独立保留。
|
||||||
|
|
||||||
|
## CR-06结果
|
||||||
|
|
||||||
|
- 删除Tushare与选股引擎内两个重复日期函数体,新增`display_compact_date`唯一策略;生产代码总
|
||||||
|
行数不增加,重复函数体由两份降为一份。
|
||||||
|
- 架构清单登记日期格式唯一所有权;保持性测试改为未改范围AST相等、共享函数AST相等和运行时
|
||||||
|
对象身份三重契约,没有放宽原迁移门禁。
|
||||||
|
- `normalize_date`、市场洞察日期显示、实时行情时间格式和会员日期边界因语义不同均原样保留。
|
||||||
|
- 候选321项、纯`app/`导出258项、24个JavaScript文件、API/架构注册表、Git空白检查和SQLite
|
||||||
|
完整性检查通过;本批不涉及页面、CSS或浏览器行为。
|
||||||
|
- 本批不修改日期输入规则、业务计算、选股结果、接口、数据库、数据源、LLM、权限或部署。
|
||||||
|
|
||||||
|
本批基线为`xiaobai-reduction-05-compatibility-boundaries-20260801`;检查点为
|
||||||
|
`xiaobai-reduction-06-date-formatting-20260801`。
|
||||||
|
|
||||||
|
## CR-07验收口径
|
||||||
|
|
||||||
|
- iFinD、图表、实时观察器和Provider适配器必须只在`build_data_gateway`创建,并由容器共享。
|
||||||
|
- Tushare必须继续通过实时Token供应器按需创建,不能为了减少对象数量缓存过期Token。
|
||||||
|
- 市场服务中为原版隔离测试桩保留的一处`TushareClient(self.token)`是明确兼容例外,不得被误判为
|
||||||
|
第二条正式数据链路。
|
||||||
|
- 不得合并Tushare、iFinD、东方财富和腾讯的传输、缓存、重试或降级逻辑。
|
||||||
|
|
||||||
|
## CR-07结果
|
||||||
|
|
||||||
|
- 全后端构造点扫描确认iFinD、MarketChart、东方财富图表和实时观察器均只有网关一个创建位置;
|
||||||
|
`ApplicationContainer`暴露的是同一对象引用,没有第二份客户端。
|
||||||
|
- Tushare Provider使用动态Token供应器,市场服务只有一处已登记测试兼容回退;本批没有发现可安全
|
||||||
|
删除的生产实现,因此不为追求行数强行修改运行代码。
|
||||||
|
- 架构清单新增Provider创建所有权,自动测试会在未来出现第二个未登记构造点时失败。
|
||||||
|
- 候选322项、纯`app/`导出259项、24个JavaScript文件、API/架构注册表、Git空白检查和SQLite
|
||||||
|
完整性检查通过;本批不涉及页面、CSS或浏览器行为。
|
||||||
|
- 本批不修改请求频率、缓存、重试、Token更新、数据源选择、计算口径、API、数据库或前端。
|
||||||
|
|
||||||
|
本批基线为`xiaobai-reduction-06-date-formatting-20260801`;检查点为
|
||||||
|
`xiaobai-reduction-07-provider-ownership-20260801`。
|
||||||
|
|
||||||
## 人工验收记录
|
## 人工验收记录
|
||||||
|
|
||||||
- 2026-08-01:用户检查CR-02与CR-03运行结果,确认未发现明显异常。本记录仅表示本轮可见功能与
|
- 2026-08-01:用户检查CR-02与CR-03运行结果,确认未发现明显异常。本记录仅表示本轮可见功能与
|
||||||
|
|||||||
Reference in New Issue
Block a user