diff --git a/.env.example b/.env.example index 0916994..64f507d 100644 --- a/.env.example +++ b/.env.example @@ -3,19 +3,41 @@ APP_HOST=0.0.0.0 APP_PORT=8080 APP_DEBUG=false -# Elasticsearch +# 功能手板 gRPC(与 HTTP 同进程;明文无鉴权,生产勿对公网裸暴露) +GRPC_HOST=0.0.0.0 +GRPC_PORT=50065 +GRPC_ENABLED=true + +# 量产 gRPC +SOMNI_GRPC_PORT=50064 +SOMNI_GRPC_ENABLED=true + +# Elasticsearch(功能手板) ES_NODE=http://localhost:9200 ES_AUDIO_INDEX=somni_audio_materials ES_TAG_VECTORS_INDEX=somni_audio_tag_dictionary -# MongoDB(Somni 数据同步源) +# 量产 ES(与手板隔离;可填另一节点) +SOMNI_ES_NODE=http://localhost:9201 +SOMNI_ES_AUDIO_INDEX=somni_audio_materials +SOMNI_ES_TAG_VECTORS_INDEX=somni_audio_tag_dictionary +SOMNI_ES_SEARCH_EVENTS_INDEX=somni_audio_search_events + +# MongoDB(功能手板 Fullive) MONGO_URI=mongodb://user:password@host:27017/Fullive MONGO_DB=Fullive -# comm-service gRPC -COMM_GRPC_HOST=bionode-test.fulai.tech -COMM_GRPC_PORT=443 -COMM_GRPC_USE_TLS=true +# MongoDB(量产 Somni) +SOMNI_MONGO_URI=mongodb://user:password@host:27017/Somni +SOMNI_MONGO_DB=Somni +# 答卷集合(对齐 BioNode quiz_answers 结构) +SOMNI_MONGO_ANSWERS_COLLECTION=somni_quiz_answers +# 量产报告相关集合 +SOMNI_MONGO_DEVICES_COLLECTION=somni_devices +SOMNI_MONGO_TELEMETRY_COLLECTION=somni_telemetry +SOMNI_MONGO_RECORDS_COLLECTION=somni_records +SOMNI_MONGO_SLEEP_REPORTS_COLLECTION=somni_sleep_reports +SOMNI_MONGO_EVENTS_COLLECTION=somni_events # 检索参数 SIM_THRESHOLD=0.7 @@ -53,8 +75,18 @@ LOG_LEVEL=INFO LOG_DIR=logs LOG_RETENTION=7 days -# 音频检索缓存(空 REDIS_URL 表示关闭;TTL 自写入起算,命中不续期) +# 功能手板 / HTTP 检索缓存(空 REDIS_URL 表示关闭;TTL 自写入起算,命中不续期) REDIS_URL=redis://127.0.0.1:6379/0 +# 量产 Redis(独立实例;空则 GetHot 关闭,不回退 REDIS_URL) +# 本地可另起:redis-server --port 6380 --save "" --appendonly no +SOMNI_REDIS_URL=redis://127.0.0.1:6380/0 +# GetHot 热点排行(Redis ZSET + ES 搜索事件索引) +SOMNI_HOT_ENABLED=true +SOMNI_HOT_TOP_N=10 +SOMNI_HOT_REDIS_KEY=somni:audio:hot:v1 +SOMNI_REDIS_MAX_CONNECTIONS=128 +SOMNI_REDIS_CONNECT_TIMEOUT_SEC=2 +SOMNI_REDIS_SOCKET_TIMEOUT_SEC=2 # 连接池大小:需 ≥ HTTP 并发峰值,过小会 Too many connections REDIS_MAX_CONNECTIONS=512 SEARCH_CACHE_MAX_SIZE=2048 diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..392bcf4 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,75 @@ +name: Deploy UburNode (dev) + +on: + workflow_dispatch: + push: + branches: + - dev + +permissions: + contents: read + +env: + # 测试机大盘路径(根分区仅 8G,Docker 与代码均放 localssd) + DEPLOY_DIR: /mnt/localssd/uburnode + # 服务器侧用 SSH 拉代码(需已配置 ~/.ssh/config 或默认可用的 GitHub 部署钥) + GIT_REPO: git@github.com:fulai-tech/UburPython.git + DEPLOY_BRANCH: dev + +jobs: + deploy: + runs-on: ubuntu-latest + concurrency: + group: uburnode-deploy-dev + cancel-in-progress: true + + steps: + - name: Validate required secrets + run: | + test -n "${{ secrets.SSH_HOST_DEV }}" || (echo "Missing SSH_HOST_DEV" && exit 1) + test -n "${{ secrets.SSH_USER_DEV }}" || (echo "Missing SSH_USER_DEV" && exit 1) + test -n "${{ secrets.SSH_PRIVATE_KEY_DEV }}" || (echo "Missing SSH_PRIVATE_KEY_DEV" && exit 1) + + - name: Deploy on server + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.SSH_HOST_DEV }} + username: ${{ secrets.SSH_USER_DEV }} + key: ${{ secrets.SSH_PRIVATE_KEY_DEV }} + port: ${{ secrets.SSH_PORT_DEV || 22 }} + command_timeout: 90m + script: | + set -euo pipefail + DEPLOY_DIR="${{ secrets.DEPLOY_PATH_DEV || env.DEPLOY_DIR }}" + GIT_REPO="${{ env.GIT_REPO }}" + DEPLOY_BRANCH="${{ env.DEPLOY_BRANCH }}" + + if [ ! -d "$DEPLOY_DIR/.git" ]; then + if [ -d "$DEPLOY_DIR" ] && [ -n "$(ls -A "$DEPLOY_DIR" 2>/dev/null)" ]; then + echo "错误: $DEPLOY_DIR 已存在但不是 git 仓库。" + echo "请备份 .env 后: rm -rf $DEPLOY_DIR && git clone -b $DEPLOY_BRANCH $GIT_REPO $DEPLOY_DIR" + exit 1 + fi + mkdir -p "$DEPLOY_DIR" + git clone -b "$DEPLOY_BRANCH" "$GIT_REPO" "$DEPLOY_DIR" + fi + + cd "$DEPLOY_DIR" + # 统一为 SSH remote;本地改动一律丢弃,强制对齐远程 DEPLOY_BRANCH + git remote set-url origin "$GIT_REPO" + git fetch --all --prune + # -f 必须:否则工作区有改动(如 docker-compose.yml)时 checkout 会直接失败 + git checkout -f -B "$DEPLOY_BRANCH" "origin/$DEPLOY_BRANCH" + git reset --hard "origin/$DEPLOY_BRANCH" + # 清理会挡住检出的未跟踪文件;保留 .env / logs / models(本地配置、日志、已上传 ONNX) + git clean -fd -e .env -e .env.* -e logs -e logs/ -e models -e models/ + + if [ ! -f .env ]; then + echo "缺少 $DEPLOY_DIR/.env,请从 .env.example 复制并填写后重试" + exit 1 + fi + + # 测试机用预置 ONNX(Dockerfile.server),避免构建期导出占满磁盘/耗时 + docker compose -f docker-compose.yml -f docker-compose.prebuilt.yml up -d --build --remove-orphans + docker image prune -f + docker compose -f docker-compose.yml -f docker-compose.prebuilt.yml ps diff --git a/README.md b/README.md index 2936ef2..0bf8aa6 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,48 @@ # UburPython -BioNode 体系中的 **Somni 音频检索服务**:以三维度检索为核心,从 MongoDB 同步 Somni 原料与标签词典至 Elasticsearch,对外提供 HTTP 检索 API。 +**Somni / 功能手板音频检索服务**:三维度检索为核心;HTTP 保留;同进程再对外暴露功能手板与量产两套 gRPC。 -- **核心**:三维度音频检索(`somni_audio_materials` ES 召回 + 标签词典向量 + 四步精排流水线) -- **数据源**:MongoDB `Fullive` 库(`somni_audio_materials`、`somni_audio_tag_dictionary`) -- **索引**:Elasticsearch `somni_audio_materials`、`somni_audio_tag_dictionary`(字段含义见 mapping `meta.description`) -- **写路径**:`POST/PUT /api/audio` 写 Mongo `somni_audio_materials`(Somni 文档体);有 `audio_url` 时同步 upsert ES;`DELETE` 仍经 comm-service +- **核心**:三维度音频检索(ES 召回 + 标签词典向量 + 精排) +- **功能手板**:`MONGO_*` + `ES_NODE` + `REDIS_URL`;HTTP `:8080` + gRPC `:50065` +- **量产**:`SOMNI_MONGO_*` + `SOMNI_ES_*` + `SOMNI_REDIS_URL`(独立 Redis 实例);gRPC `:50064` +- **写路径**:直连 Mongo,再同步本侧 ES(不再调用 BioNode) +- **接口文档**:`docs/功能手板接口文档.md`、`docs/量产接口文档.md` ## 架构 ```text 算法端 / 调用方 │ - ▼ -对外 HTTP (FastAPI + Pydantic) - │ - ├──读(检索)──► somni_audio_materials (ES) - │ + somni_audio_tag_dictionary (ES 向量) - │ + 进程内 Embedding (bge-small-zh-v1.5) - │ - ├──写(创建/更新)──► Mongo somni_audio_materials ──► ES upsert(有 audio_url) - ├──写(删除)──► comm-service (gRPC) + ES delete - │ - └──同步(定时/手动)──► MongoDB Somni 集合 ──► Elasticsearch + ├── HTTP :8080 ──► app/api/audio ──► server/handboard(Fullive) + ├── gRPC :50065 ─► uburnode.v1(手板 audio/quiz) + └── gRPC :50064 ─► uburnode.somni.v1(量产 ListTags/ListAudios/Search/GetAnswer) ``` -## 数据流 - -| 环节 | 来源 | 目标 | 模块 | -|------|------|------|------| -| Mongo → ES 同步 | `somni_audio_materials`、`somni_audio_tag_dictionary` | 同名 ES 索引 | `scripts/sync_es_from_comm.py` | -| 标签向量 | 词典 `name` / `name_en` | `name_vector` / `name_en_vector` | 同步脚本 + `app/embedding/` | -| HTTP 检索 | ES 原料文档 | `data.results[].materials[]` 原样返回 | `app/services/retrieval.py` | -| HTTP 创建/更新 | Somni 文档体(仅创建强制 `audio_name`) | Mongo + ES | `app/services/audio.py` | -| HTTP 删除(遗留) | material_id | comm + ES delete | `app/services/audio.py` | - -字段命名全链路 **snake_case**。Somni 表结构详见仓库内 `音频表结构.md`。 - ## 目录结构 ```text UburPython/ ├── app/ -│ ├── main.py # FastAPI 入口 + lifespan -│ ├── core/ # 配置、日志、标签转换 -│ ├── api/audio.py # 4 个 HTTP 端点 -│ ├── schemas/audio.py # Pydantic 模型 -│ ├── services/ # AudioService、RetrievalService -│ ├── mongo/materials.py # Mongo somni_audio_materials 读写 -│ ├── es/ -│ │ ├── search.py # EsSearch 读路径 -│ │ ├── sync.py # EsSync(Somni upsert + 删除) -│ │ ├── somni_docs.py # description_text 等文档转换 -│ │ └── index_mappings.py # ES 索引 mapping + 字段注释 -│ ├── embedding/encoder.py # bge-small-zh-v1.5 向量编码 -│ └── bionode_grpc_clients/ # comm-service gRPC 客户端 +│ ├── main.py # FastAPI + lifespan(双 gRPC) +│ ├── api/audio.py # HTTP /api/audio +│ ├── server/ +│ │ ├── bootstrap.py # 启停手板/量产 gRPC +│ │ ├── handboard/ # 功能手板 audio|quiz +│ │ └── somni/ # 量产 audio|quiz +│ ├── uburnode_grpc/grpc_gen/ # proto 生成 stub +│ ├── core/ # 配置、日志、bson 工具 +│ ├── schemas/ +│ ├── services/retrieval.py # 检索流水线 +│ ├── es/ # ES 读/写同步 +│ ├── embedding/ +│ ├── cache/ +│ └── middleware/ ├── scripts/ │ ├── sync_es_from_comm.py # Mongo → ES 差异同步 -│ └── gen_proto.sh # 生成 gRPC stub -├── proto/ # bionode_comm.proto +│ └── gen_uburnode_proto.sh # 生成对外 gRPC stub +├── proto/ +│ ├── uburnode.proto +│ └── uburnode_somni.proto ├── tests/ ├── pyproject.toml └── .env.example @@ -70,9 +54,9 @@ UburPython/ # 1. 安装依赖(推荐 uv) uv sync --extra dev -# 2. 生成 comm gRPC stub(CUD 接口需要) -chmod +x scripts/gen_proto.sh -./scripts/gen_proto.sh +# 2. 生成对外 gRPC stub +chmod +x scripts/gen_uburnode_proto.sh +./scripts/gen_uburnode_proto.sh # 3. 本地 Elasticsearch docker compose -f docker-compose.es.yml up -d @@ -80,12 +64,12 @@ curl -s http://localhost:9200 # 4. 配置环境变量 cp .env.example .env -# 编辑 ES_NODE、MONGO_URI、EMBEDDING_ONNX_DIR、COMM_GRPC_* 等 +# 编辑 ES_NODE、MONGO_URI、SOMNI_MONGO_URI、SOMNI_ES_NODE、EMBEDDING_* 等 # 5. 导出 ONNX 模型(若 models/ 目录尚无模型) # 见 scripts/export_onnx_model.py -# 6. Mongo → ES 全量同步 +# 6. Mongo → ES 同步(手板库) uv run python scripts/sync_es_from_comm.py --dry-run uv run python scripts/sync_es_from_comm.py diff --git a/app/api/audio.py b/app/api/audio.py index 94b1a28..6069393 100644 --- a/app/api/audio.py +++ b/app/api/audio.py @@ -13,7 +13,7 @@ UpdateAudioRequest, ) from app.schemas.response import ApiResponse, success -from app.services.audio import AudioService +from app.server.handboard.audio.service import AudioService router = APIRouter(prefix="/audio", tags=["audio"]) diff --git a/app/bionode_grpc_clients/__init__.py b/app/bionode_grpc_clients/__init__.py deleted file mode 100644 index 02b59bf..0000000 --- a/app/bionode_grpc_clients/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""BioNode 外部微服务 gRPC 客户端集成层。""" - -from app.bionode_grpc_clients.comm.client import CommClient - -__all__ = ["CommClient"] diff --git a/app/bionode_grpc_clients/comm/__init__.py b/app/bionode_grpc_clients/comm/__init__.py deleted file mode 100644 index c5dd8f0..0000000 --- a/app/bionode_grpc_clients/comm/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""comm-service gRPC 客户端。""" - -from app.bionode_grpc_clients.comm.client import ( - AUDIO_MATERIAL_STATUS_PUBLISHED, - CommClient, -) - -__all__ = ["AUDIO_MATERIAL_STATUS_PUBLISHED", "CommClient"] diff --git a/app/bionode_grpc_clients/comm/client.py b/app/bionode_grpc_clients/comm/client.py deleted file mode 100644 index 21bb7bd..0000000 --- a/app/bionode_grpc_clients/comm/client.py +++ /dev/null @@ -1,212 +0,0 @@ -"""comm-service gRPC 客户端(AudioMaterialService)。 - -UburNode 不直连 MongoDB;所有 CUD 经 comm-service(规范红线)。 -proto 真源:仓库根 proto/bionode_comm.proto,变更后须重新 gen_proto.sh。 -""" - -from __future__ import annotations - -import asyncio -from typing import Any - -import grpc -from loguru import logger - -from app.bionode_grpc_clients.comm.grpc_gen import bionode_comm_pb2, bionode_comm_pb2_grpc -from app.core.config import Settings -from app.schemas.audio import CreateAudioRequest, UpdateAudioRequest - -# comm ListAudioMaterials:按启用态筛选;与 BioNode bool status 对齐 -AUDIO_MATERIAL_STATUS_PUBLISHED = True - - -class CommClient: - """封装 AudioMaterialService 的 gRPC 调用。""" - - def __init__(self, settings: Settings) -> None: - self._settings = settings - self._channel: grpc.aio.Channel | None = None - self._stub: bionode_comm_pb2_grpc.AudioMaterialServiceStub | None = None - - async def connect(self) -> None: - target = self._settings.comm_grpc_target - tls = self._settings.comm_grpc_use_tls - logger.info("正在连接 comm-service gRPC:{}(TLS={})", target, tls) - if tls: - credentials = grpc.ssl_channel_credentials() - self._channel = grpc.aio.secure_channel(target, credentials) - else: - self._channel = grpc.aio.insecure_channel(target) - self._stub = bionode_comm_pb2_grpc.AudioMaterialServiceStub(self._channel) - - async def ping(self, timeout_sec: float = 10.0) -> int: - """探测 comm-service:调用 GetDistinctTags(首次 RPC 时建连)。""" - if self._stub is None: - raise RuntimeError("CommClient 未连接,请先调用 connect()") - response = await asyncio.wait_for( - self._stub.GetDistinctTags(bionode_comm_pb2.EmptyReq()), - timeout=timeout_sec, - ) - return len(response.tags) - - async def close(self) -> None: - if self._channel is not None: - await self._channel.close() - self._channel = None - self._stub = None - - def _require_stub(self) -> bionode_comm_pb2_grpc.AudioMaterialServiceStub: - if self._stub is None: - raise RuntimeError("CommClient 未连接,请先在 lifespan 中调用 connect()") - return self._stub - - async def get_audio_material(self, material_id: str) -> bionode_comm_pb2.AudioMaterialInfo: - stub = self._require_stub() - response = await stub.GetAudioMaterial(bionode_comm_pb2.IdReq(id=material_id)) - return response.material - - async def create_audio_material(self, request: CreateAudioRequest) -> None: - stub = self._require_stub() - await stub.CreateAudioMaterial(_to_create_req(request)) - - async def update_audio_material( - self, material_id: str, request: UpdateAudioRequest - ) -> None: - stub = self._require_stub() - await stub.UpdateAudioMaterial(_to_update_req(material_id, request)) - - async def delete_audio_material(self, material_id: str) -> None: - stub = self._require_stub() - await stub.DeleteAudioMaterial(bionode_comm_pb2.IdReq(id=material_id)) - - async def list_audio_materials_page( - self, - *, - page: int = 1, - page_size: int = 100, - ) -> tuple[list[bionode_comm_pb2.AudioMaterialInfo], int]: - """分页拉取已发布原料;返回 (materials, total)。""" - stub = self._require_stub() - from app.bionode_grpc_clients.comm.grpc_gen import bionode_common_pb2 - - response = await stub.ListAudioMaterials( - bionode_comm_pb2.ListAudioMaterialsReq( - page=bionode_common_pb2.PageRequest( - page=page, - page_size=page_size, - order_by="update_time desc", - ), - status=AUDIO_MATERIAL_STATUS_PUBLISHED, - ) - ) - total = response.page.total if response.HasField("page") else len(response.materials) - return list(response.materials), total - - async def list_audio_materials_by_name( - self, name: str - ) -> list[bionode_comm_pb2.AudioMaterialInfo]: - """Create 返回 EmptyRes 时的临时反查方案,待 proto 扩展后移除。""" - stub = self._require_stub() - from app.bionode_grpc_clients.comm.grpc_gen import bionode_common_pb2 - - response = await stub.ListAudioMaterials( - bionode_comm_pb2.ListAudioMaterialsReq( - page=bionode_common_pb2.PageRequest( - page=1, - page_size=10, - order_by="create_time desc", - ), - name=name, - status=AUDIO_MATERIAL_STATUS_PUBLISHED, - ) - ) - return list(response.materials) - - -def _to_create_req(request: CreateAudioRequest) -> bionode_comm_pb2.CreateAudioMaterialReq: - payload = request.model_dump(exclude_none=True) - req = bionode_comm_pb2.CreateAudioMaterialReq( - audio_name=request.audio_name, - description=payload.get("description", ""), - audio_url=payload.get("audio_url", ""), - operation_type=int(payload.get("operation_type", 0)), - created_by=payload.get("created_by", ""), - updated_by=payload.get("updated_by", ""), - ) - _copy_tag_fields(req, payload) - _copy_embedding(req, payload) - return req - - -def _to_update_req( - material_id: str, request: UpdateAudioRequest -) -> bionode_comm_pb2.UpdateAudioMaterialReq: - fields = request.model_dump(exclude_unset=True) - req = bionode_comm_pb2.UpdateAudioMaterialReq(id=material_id) - _set_optional_str(req, "description", fields) - _set_optional_str(req, "audio_name", fields) - _set_optional_str(req, "audio_url", fields) - _set_optional_str(req, "created_by", fields) - _set_optional_str(req, "updated_by", fields) - if "operation_type" in fields and fields["operation_type"] is not None: - req.operation_type = int(fields["operation_type"]) - if "status" in fields and fields["status"] is not None: - req.status = bool(fields["status"]) - _copy_tag_fields(req, fields) - _copy_embedding(req, fields) - return req - - -def _set_optional_str(req: Any, field: str, fields: dict[str, Any]) -> None: - if field not in fields or fields[field] is None: - return - setattr(req, field, str(fields[field])) - - -def _copy_tag_fields(req: Any, fields: dict[str, Any]) -> None: - mapping = ( - "sleep_stage_tags", - "content_form_tags", - "mechanism_tags", - "audio_engineering_tags", - "medical_risk_tags", - "evidence_level_tags", - ) - for name in mapping: - if name not in fields: - continue - getattr(req, name).extend(_to_proto_tags(fields[name] or [])) - - -def _copy_embedding(req: Any, fields: dict[str, Any]) -> None: - if "embedding" not in fields: - return - req.embedding[:] = [float(v) for v in fields["embedding"] or []] - - -def _to_proto_tags(tags: list[dict[str, Any]]) -> list[bionode_comm_pb2.AudioMaterialTag]: - return [_to_proto_tag(tag) for tag in tags] - - -def _to_proto_tag(tag: dict[str, Any]) -> bionode_comm_pb2.AudioMaterialTag: - msg = bionode_comm_pb2.AudioMaterialTag( - tag_id=str(tag.get("tag_id") or ""), - code=str(tag.get("code") or ""), - name=str(tag.get("name") or ""), - ) - if tag.get("en_name") is not None: - msg.en_name = str(tag["en_name"]) - if tag.get("parent_tag_id") is not None: - msg.parent_tag_id = str(tag["parent_tag_id"]) - if tag.get("parent_tag_code") is not None: - msg.parent_tag_code = str(tag["parent_tag_code"]) - if tag.get("relative_loudness") is not None: - msg.relative_loudness = float(tag["relative_loudness"]) - if tag.get("band_values"): - msg.band_values[:] = [float(v) for v in tag["band_values"]] - value = tag.get("value") - if isinstance(value, dict): - msg.value.tag_id = str(value.get("tag_id") or "") - msg.value.code = str(value.get("code") or "") - msg.value.name = str(value.get("name") or "") - return msg diff --git a/app/bionode_grpc_clients/comm/grpc_gen/__init__.py b/app/bionode_grpc_clients/comm/grpc_gen/__init__.py deleted file mode 100644 index a5a44f5..0000000 --- a/app/bionode_grpc_clients/comm/grpc_gen/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# gRPC stub 由 scripts/gen_proto.sh 生成 diff --git a/app/bionode_grpc_clients/comm/grpc_gen/bionode_comm_pb2.py b/app/bionode_grpc_clients/comm/grpc_gen/bionode_comm_pb2.py deleted file mode 100644 index 698cf15..0000000 --- a/app/bionode_grpc_clients/comm/grpc_gen/bionode_comm_pb2.py +++ /dev/null @@ -1,573 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: bionode_comm.proto -# Protobuf Python Version: 6.33.5 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 33, - 5, - '', - 'bionode_comm.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from . import bionode_common_pb2 as bionode__common__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x62ionode_comm.proto\x12\x0f\x62ionode.comm.v1\x1a\x14\x62ionode_common.proto\"\x1c\n\x0bMhrCodesRes\x12\r\n\x05\x63odes\x18\x01 \x03(\t\"\x13\n\x05IdReq\x12\n\n\x02id\x18\x01 \x01(\t\"\n\n\x08\x45mptyReq\"\n\n\x08\x45mptyRes\"\x19\n\x08\x43ountRes\x12\r\n\x05\x63ount\x18\x01 \x01(\x03\"f\n\x0eQuestionOption\x12\x11\n\toption_id\x18\x01 \x01(\t\x12\x13\n\x0boption_text\x18\x02 \x01(\t\x12\x12\n\nsort_order\x18\x03 \x01(\x05\x12\x18\n\x10is_input_enabled\x18\x04 \x01(\x08\"\xed\x01\n\nPickerItem\x12\r\n\x05index\x18\x03 \x01(\x05\x12\r\n\x05label\x18\x01 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\x12\r\n\x05title\x18\x04 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\x12\n\ninput_type\x18\x06 \x01(\t\x12\x30\n\x07options\x18\x07 \x03(\x0b\x32\x1f.bionode.comm.v1.QuestionOption\x12/\n\x06\x63onfig\x18\x08 \x01(\x0b\x32\x1f.bionode.comm.v1.QuestionConfig\x12\x16\n\x0eis_extra_input\x18\t \x01(\x08\"\x99\x03\n\x0eQuestionConfig\x12\x18\n\x10max_select_count\x18\x01 \x01(\x05\x12\x13\n\x0bplaceholder\x18\x02 \x01(\t\x12\x12\n\nmax_length\x18\x03 \x01(\x05\x12\x0b\n\x03min\x18\x04 \x01(\x05\x12\x0b\n\x03max\x18\x05 \x01(\x05\x12\x0c\n\x04step\x18\x06 \x01(\x05\x12\x0c\n\x04unit\x18\x07 \x01(\t\x12\x11\n\tmax_value\x18\x08 \x01(\x05\x12\x12\n\nallow_half\x18\t \x01(\x08\x12\x0e\n\x06levels\x18\n \x03(\t\x12\x13\n\x0b\x64\x61ta_source\x18\x0b \x01(\t\x12;\n\x06labels\x18\x0c \x03(\x0b\x32+.bionode.comm.v1.QuestionConfig.LabelsEntry\x12*\n\x05items\x18\x0e \x03(\x0b\x32\x1b.bionode.comm.v1.PickerItem\x12\x13\n\x0b\x61\x63tive_text\x18\x0f \x01(\t\x12\x15\n\rinactive_text\x18\x10 \x01(\t\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe7\x02\n\x0cQuestionInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x12\n\ninput_type\x18\x04 \x01(\t\x12\x15\n\rbusiness_type\x18\x05 \x01(\t\x12\x11\n\tdimension\x18\x06 \x01(\t\x12\x30\n\x07options\x18\x07 \x03(\x0b\x32\x1f.bionode.comm.v1.QuestionOption\x12/\n\x06\x63onfig\x18\x08 \x01(\x0b\x32\x1f.bionode.comm.v1.QuestionConfig\x12\x0c\n\x04tags\x18\t \x03(\t\x12\x10\n\x08language\x18\n \x01(\t\x12\x0e\n\x06status\x18\x0b \x01(\x05\x12\x13\n\x0b\x63reate_time\x18\x0c \x01(\t\x12\x13\n\x0bupdate_time\x18\r \x01(\t\x12\x14\n\x0cscoring_type\x18\x0e \x01(\t\x12\x16\n\x0eis_extra_input\x18\x0f \x01(\x08\"@\n\x15GetActiveQuestionsReq\x12\x15\n\rbusiness_type\x18\x01 \x01(\t\x12\x10\n\x08language\x18\x02 \x01(\t\"\xa0\x01\n\x10ListQuestionsReq\x12,\n\x04page\x18\x01 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\x12\x15\n\rbusiness_type\x18\x02 \x01(\t\x12\x12\n\ninput_type\x18\x03 \x01(\t\x12\x11\n\tdimension\x18\x04 \x01(\t\x12\x0e\n\x06status\x18\x05 \x01(\x05\x12\x10\n\x08language\x18\x06 \x01(\t\"\x90\x02\n\x11\x43reateQuestionReq\x12\r\n\x05title\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x12\n\ninput_type\x18\x03 \x01(\t\x12\x15\n\rbusiness_type\x18\x04 \x01(\t\x12\x11\n\tdimension\x18\x05 \x01(\t\x12\x30\n\x07options\x18\x06 \x03(\x0b\x32\x1f.bionode.comm.v1.QuestionOption\x12/\n\x06\x63onfig\x18\x07 \x01(\x0b\x32\x1f.bionode.comm.v1.QuestionConfig\x12\x0c\n\x04tags\x18\x08 \x03(\t\x12\x10\n\x08language\x18\t \x01(\t\x12\x16\n\x0eis_extra_input\x18\n \x01(\x08\"\xac\x02\n\x11UpdateQuestionReq\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x12\n\ninput_type\x18\x04 \x01(\t\x12\x15\n\rbusiness_type\x18\x05 \x01(\t\x12\x11\n\tdimension\x18\x06 \x01(\t\x12\x30\n\x07options\x18\x07 \x03(\x0b\x32\x1f.bionode.comm.v1.QuestionOption\x12/\n\x06\x63onfig\x18\x08 \x01(\x0b\x32\x1f.bionode.comm.v1.QuestionConfig\x12\x0c\n\x04tags\x18\t \x03(\t\x12\x10\n\x08language\x18\n \x01(\t\x12\x0e\n\x06status\x18\x0b \x01(\x05\x12\x16\n\x0eis_extra_input\x18\x0c \x01(\x08\"5\n\x17UpdateQuestionStatusReq\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\x05\"\'\n\x18GetQuestionsByIdsRequest\x12\x0b\n\x03ids\x18\x01 \x03(\t\">\n\x0bQuestionRes\x12/\n\x08question\x18\x01 \x01(\x0b\x32\x1d.bionode.comm.v1.QuestionInfo\"r\n\x0fQuestionListRes\x12\x30\n\tquestions\x18\x01 \x03(\x0b\x32\x1d.bionode.comm.v1.QuestionInfo\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"E\n\x10ItemDisplayRules\x12\x1e\n\x16\x64\x65pends_on_question_id\x18\x01 \x01(\t\x12\x11\n\toption_id\x18\x02 \x01(\t\"\x97\x01\n\x0eSurveyPageItem\x12\x13\n\x0bquestion_id\x18\x01 \x01(\t\x12\x12\n\nsort_order\x18\x02 \x01(\x05\x12\x13\n\x0bis_required\x18\x03 \x01(\x08\x12\r\n\x05\x61lias\x18\x04 \x01(\t\x12\x38\n\rdisplay_rules\x18\x05 \x01(\x0b\x32!.bionode.comm.v1.ItemDisplayRules\"~\n\nSurveyPage\x12\x12\n\npage_index\x18\x01 \x01(\x05\x12\x12\n\npage_title\x18\x02 \x01(\t\x12\x18\n\x10page_description\x18\x03 \x01(\t\x12.\n\x05items\x18\x04 \x03(\x0b\x32\x1f.bionode.comm.v1.SurveyPageItem\"t\n\x0eSurveySettings\x12\x14\n\x0c\x61llow_resume\x18\x01 \x01(\x08\x12\x15\n\rshow_progress\x18\x02 \x01(\x08\x12\x19\n\x11randomize_options\x18\x03 \x01(\x08\x12\x1a\n\x12time_limit_minutes\x18\x04 \x01(\x05\"G\n\x0e\x43odeRuleOption\x12\x11\n\toption_id\x18\x01 \x01(\t\x12\r\n\x05score\x18\x02 \x01(\x05\x12\x13\n\x0blabel_value\x18\x03 \x01(\t\"Y\n\x10\x43odeRuleQuestion\x12\x13\n\x0bquestion_id\x18\x01 \x01(\t\x12\x30\n\x07options\x18\x02 \x03(\x0b\x32\x1f.bionode.comm.v1.CodeRuleOption\"F\n\rThresholdItem\x12\x0b\n\x03min\x18\x01 \x01(\x05\x12\x0b\n\x03max\x18\x02 \x01(\x05\x12\r\n\x05label\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x65sc\x18\x04 \x01(\t\"\xee\x01\n\x08\x43odeRule\x12\x10\n\x08\x63ode_key\x18\x01 \x01(\t\x12\x11\n\trule_type\x18\x02 \x01(\t\x12\x0f\n\x07in_code\x18\x03 \x01(\x05\x12\x34\n\tquestions\x18\x04 \x03(\x0b\x32!.bionode.comm.v1.CodeRuleQuestion\x12\x32\n\nthresholds\x18\x05 \x03(\x0b\x32\x1e.bionode.comm.v1.ThresholdItem\x12\x15\n\rdefault_label\x18\x06 \x01(\t\x12\x11\n\trule_name\x18\x07 \x01(\t\x12\x18\n\x10score_multiplier\x18\x08 \x01(\x01\":\n\x0fResultGuideItem\x12\x0b\n\x03min\x18\x01 \x01(\x05\x12\x0b\n\x03max\x18\x02 \x01(\x05\x12\r\n\x05label\x18\x03 \x01(\t\"^\n\x0bResultGuide\x12\r\n\x05title\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12/\n\x05items\x18\x03 \x03(\x0b\x32 .bionode.comm.v1.ResultGuideItem\"\xa3\x03\n\nSurveyInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0c\n\x04\x63ode\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x15\n\rbusiness_type\x18\x05 \x01(\t\x12\x13\n\x0b\x63over_image\x18\x06 \x01(\t\x12*\n\x05pages\x18\x07 \x03(\x0b\x32\x1b.bionode.comm.v1.SurveyPage\x12\x31\n\x08settings\x18\x08 \x01(\x0b\x32\x1f.bionode.comm.v1.SurveySettings\x12\x1c\n\x14total_question_count\x18\t \x01(\x05\x12.\n\x0brule_config\x18\n \x03(\x0b\x32\x19.bionode.comm.v1.CodeRule\x12\x10\n\x08language\x18\x0b \x01(\t\x12\x0e\n\x06status\x18\x0c \x01(\x05\x12\x13\n\x0b\x63reate_time\x18\r \x01(\t\x12\x13\n\x0bupdate_time\x18\x0e \x01(\t\x12\x32\n\x0cresult_guide\x18\x0f \x01(\x0b\x32\x1c.bionode.comm.v1.ResultGuide\"\x1a\n\x0cGetSurveyReq\x12\n\n\x02id\x18\x01 \x01(\t\"4\n\x12GetSurveyByCodeReq\x12\x0c\n\x04\x63ode\x18\x01 \x01(\t\x12\x10\n\x08language\x18\x02 \x01(\t\"w\n\x0eListSurveysReq\x12,\n\x04page\x18\x01 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\x12\x15\n\rbusiness_type\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12\x10\n\x08language\x18\x04 \x01(\t\"\xc4\x02\n\x0f\x43reateSurveyReq\x12\r\n\x05title\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x15\n\rbusiness_type\x18\x04 \x01(\t\x12\x13\n\x0b\x63over_image\x18\x05 \x01(\t\x12*\n\x05pages\x18\x06 \x03(\x0b\x32\x1b.bionode.comm.v1.SurveyPage\x12\x31\n\x08settings\x18\x07 \x01(\x0b\x32\x1f.bionode.comm.v1.SurveySettings\x12\x10\n\x08language\x18\x08 \x01(\t\x12.\n\x0brule_config\x18\t \x03(\x0b\x32\x19.bionode.comm.v1.CodeRule\x12\x32\n\x0cresult_guide\x18\n \x01(\x0b\x32\x1c.bionode.comm.v1.ResultGuide\"\xe0\x02\n\x0fUpdateSurveyReq\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0c\n\x04\x63ode\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x15\n\rbusiness_type\x18\x05 \x01(\t\x12\x13\n\x0b\x63over_image\x18\x06 \x01(\t\x12*\n\x05pages\x18\x07 \x03(\x0b\x32\x1b.bionode.comm.v1.SurveyPage\x12\x31\n\x08settings\x18\x08 \x01(\x0b\x32\x1f.bionode.comm.v1.SurveySettings\x12\x10\n\x08language\x18\t \x01(\t\x12\x0e\n\x06status\x18\n \x01(\x05\x12.\n\x0brule_config\x18\x0b \x03(\x0b\x32\x19.bionode.comm.v1.CodeRule\x12\x32\n\x0cresult_guide\x18\x0c \x01(\x0b\x32\x1c.bionode.comm.v1.ResultGuide\"3\n\x15UpdateSurveyStatusReq\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\x05\"8\n\tSurveyRes\x12+\n\x06survey\x18\x01 \x01(\x0b\x32\x1b.bionode.comm.v1.SurveyInfo\"l\n\rSurveyListRes\x12,\n\x07surveys\x18\x01 \x03(\x0b\x32\x1b.bionode.comm.v1.SurveyInfo\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"\x91\x01\n\x17SurveyPageWithQuestions\x12\x12\n\npage_index\x18\x01 \x01(\x05\x12\x12\n\npage_title\x18\x02 \x01(\t\x12\x18\n\x10page_description\x18\x03 \x01(\t\x12\x34\n\tquestions\x18\x04 \x03(\x0b\x32!.bionode.comm.v1.QuestionWithMeta\"\\\n\x0c\x44isplayRules\x12\x12\n\ndepends_on\x18\x01 \x01(\t\x12\x1e\n\x16\x64\x65pends_on_question_id\x18\x02 \x01(\t\x12\x18\n\x10show_when_option\x18\x03 \x01(\t\"\xb1\x01\n\x10QuestionWithMeta\x12/\n\x08question\x18\x01 \x01(\x0b\x32\x1d.bionode.comm.v1.QuestionInfo\x12\x12\n\nsort_order\x18\x02 \x01(\x05\x12\x13\n\x0bis_required\x18\x03 \x01(\x08\x12\r\n\x05\x61lias\x18\x04 \x01(\t\x12\x34\n\rdisplay_rules\x18\x05 \x01(\x0b\x32\x1d.bionode.comm.v1.DisplayRules\"~\n\x16SurveyWithQuestionsRes\x12+\n\x06survey\x18\x01 \x01(\x0b\x32\x1b.bionode.comm.v1.SurveyInfo\x12\x37\n\x05pages\x18\x02 \x03(\x0b\x32(.bionode.comm.v1.SurveyPageWithQuestions\"q\n\x14\x41nswerSelectedOption\x12\x0e\n\x06opt_id\x18\x01 \x01(\t\x12\x10\n\x08opt_text\x18\x02 \x01(\t\x12\r\n\x05score\x18\x03 \x01(\x01\x12\x13\n\x0blabel_value\x18\x04 \x01(\t\x12\x13\n\x0binput_value\x18\x05 \x01(\t\"\xca\x01\n\x0c\x41nswerDetail\x12\x13\n\x0bquestion_id\x18\x01 \x01(\t\x12\x12\n\nvalue_json\x18\x07 \x01(\t\x12\r\n\x05title\x18\x05 \x01(\t\x12\x18\n\x10selected_options\x18\x02 \x03(\t\x12\x13\n\x0binput_value\x18\x03 \x01(\t\x12\x15\n\rnumeric_value\x18\x04 \x01(\x01\x12<\n\ropt_snapshots\x18\x06 \x03(\x0b\x32%.bionode.comm.v1.AnswerSelectedOption\"\x8a\x02\n\nAnswerInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0b\n\x03uid\x18\x02 \x01(\t\x12\x11\n\tsurvey_id\x18\x03 \x01(\t\x12\x13\n\x0bsurvey_code\x18\x04 \x01(\t\x12\x10\n\x08language\x18\x05 \x01(\t\x12.\n\x07\x61nswers\x18\x06 \x03(\x0b\x32\x1d.bionode.comm.v1.AnswerDetail\x12\x14\n\x0cis_completed\x18\x07 \x01(\x08\x12\x13\n\x0brecord_date\x18\x08 \x01(\t\x12\x13\n\x0b\x63reate_time\x18\t \x01(\t\x12\x13\n\x0bupdate_time\x18\n \x01(\t\x12\x10\n\x08nickname\x18\x0b \x01(\t\x12\x12\n\navatar_url\x18\x0c \x01(\t\"a\n\rSaveAnswerReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0bsurvey_code\x18\x02 \x01(\t\x12.\n\x07\x61nswers\x18\x03 \x03(\x0b\x32\x1d.bionode.comm.v1.AnswerDetail\"8\n\x14GetAnswerProgressReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0bsurvey_code\x18\x02 \x01(\t\"%\n\x10GetAnswerByIdReq\x12\x11\n\tanswer_id\x18\x01 \x01(\t\"8\n\tAnswerRes\x12+\n\x06\x61nswer\x18\x01 \x01(\x0b\x32\x1b.bionode.comm.v1.AnswerInfo\"\x9c\x01\n\x0eListAnswersReq\x12,\n\x04page\x18\x01 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\x12\x0b\n\x03uid\x18\x02 \x01(\t\x12\x13\n\x0bsurvey_code\x18\x03 \x01(\t\x12\x14\n\x0cis_completed\x18\x04 \x01(\x05\x12\x12\n\nstart_date\x18\x05 \x01(\t\x12\x10\n\x08\x65nd_date\x18\x06 \x01(\t\"l\n\rAnswerListRes\x12,\n\x07\x61nswers\x18\x01 \x03(\x0b\x32\x1b.bionode.comm.v1.AnswerInfo\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"]\n\nIndicators\x12\x11\n\tcircadian\x18\x01 \x01(\x05\x12\x13\n\x0bsensitivity\x18\x02 \x01(\x05\x12\x13\n\x0b\x62rain_state\x18\x03 \x01(\x05\x12\x12\n\natmosphere\x18\x04 \x01(\x05\"O\n\x0cImproveScene\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x0e\n\x06\x63onfig\x18\x04 \x01(\t\"t\n\x0bImprovePlan\x12\r\n\x05phase\x18\x01 \x01(\t\x12\x12\n\nphase_name\x18\x02 \x01(\t\x12-\n\x06scenes\x18\x03 \x03(\x0b\x32\x1d.bionode.comm.v1.ImproveScene\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\"4\n\x10ScoreDetailEntry\x12\x11\n\tdimension\x18\x01 \x01(\t\x12\r\n\x05score\x18\x02 \x01(\x05\"\xc4\x05\n\x0eQuizResultInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0b\n\x03uid\x18\x02 \x01(\t\x12\x11\n\tsurvey_id\x18\x03 \x01(\t\x12\x13\n\x0bsurvey_code\x18\x04 \x01(\t\x12\x11\n\tanswer_id\x18\x05 \x01(\t\x12\x11\n\tmhr_codes\x18\x06 \x03(\t\x12\x10\n\x08mhr_name\x18\x07 \x01(\t\x12\x12\n\natmosphere\x18\x08 \x01(\t\x12\x0c\n\x04tags\x18\t \x03(\t\x12\x0f\n\x07\x63omment\x18\n \x01(\t\x12/\n\nindicators\x18\x0b \x01(\x0b\x32\x1b.bionode.comm.v1.Indicators\x12\x18\n\x10\x61nalysis_insight\x18\x0c \x01(\t\x12\x18\n\x10population_ratio\x18\r \x01(\x01\x12\x32\n\x0cimprove_plan\x18\x0e \x03(\x0b\x32\x1c.bionode.comm.v1.ImprovePlan\x12\x37\n\x0cscore_detail\x18\x0f \x03(\x0b\x32!.bionode.comm.v1.ScoreDetailEntry\x12\x13\n\x0b\x63reate_time\x18\x10 \x01(\t\x12\x13\n\x0bupdate_time\x18\x11 \x01(\t\x12\x10\n\x08nickname\x18\x12 \x01(\t\x12\x12\n\navatar_url\x18\x13 \x01(\t\x12\x15\n\ridentity_name\x18\x14 \x01(\t\x12\x18\n\x10\x63haracter_3d_url\x18\x15 \x01(\t\x12\x17\n\x0fstatus_analysis\x18\x16 \x01(\t\x12\x46\n\x16indicator_descriptions\x18\x17 \x01(\x0b\x32&.bionode.comm.v1.IndicatorDescriptions\x12\x32\n\x0cshare_config\x18\x18 \x01(\x0b\x32\x1c.bionode.comm.v1.ShareConfig\x12\x0e\n\x06openid\x18\x19 \x01(\t\x12\x0e\n\x06gender\x18\x1a \x01(\x05\"k\n\x12\x43omputeMhrCodesReq\x12\x13\n\x0bsurvey_code\x18\x01 \x01(\t\x12.\n\x07\x61nswers\x18\x02 \x03(\x0b\x32\x1d.bionode.comm.v1.AnswerDetail\x12\x10\n\x08language\x18\x03 \x01(\t\"\'\n\x12\x43omputeMhrCodesRes\x12\x11\n\tmhr_codes\x18\x01 \x03(\t\"V\n\rSubmitQuizReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0bsurvey_code\x18\x02 \x01(\t\x12\x11\n\tanswer_id\x18\x03 \x01(\t\x12\x10\n\x08language\x18\x04 \x01(\t\"\x81\x01\n\x12SubmitScaleQuizReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0bsurvey_code\x18\x02 \x01(\t\x12\x11\n\tanswer_id\x18\x03 \x01(\t\x12\x11\n\tscale_uid\x18\x04 \x01(\t\x12\x10\n\x08language\x18\x05 \x01(\t\x12\x11\n\ttimepoint\x18\x06 \x01(\t\"6\n\x12GetLatestResultReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0bsurvey_code\x18\x02 \x01(\t\"%\n\x10GetResultByIdReq\x12\x11\n\tresult_id\x18\x01 \x01(\t\"\x85\x01\n\x0eListResultsReq\x12,\n\x04page\x18\x01 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\x12\x0b\n\x03uid\x18\x02 \x01(\t\x12\x13\n\x0bsurvey_code\x18\x03 \x01(\t\x12\x11\n\tmhr_codes\x18\x04 \x03(\t\x12\x10\n\x08mhr_name\x18\x05 \x01(\t\"@\n\rQuizResultRes\x12/\n\x06result\x18\x01 \x01(\x0b\x32\x1f.bionode.comm.v1.QuizResultInfo\"t\n\x11QuizResultListRes\x12\x30\n\x07results\x18\x01 \x03(\x0b\x32\x1f.bionode.comm.v1.QuizResultInfo\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"h\n\x15IndicatorDescriptions\x12\x11\n\tcircadian\x18\x01 \x01(\t\x12\x13\n\x0bsensitivity\x18\x02 \x01(\t\x12\x13\n\x0b\x62rain_state\x18\x03 \x01(\t\x12\x12\n\natmosphere\x18\x04 \x01(\t\"L\n\tSceneItem\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x0e\n\x06\x63onfig\x18\x04 \x01(\t\"h\n\x0fSchemeColorStop\x12\t\n\x01r\x18\x01 \x01(\x05\x12\t\n\x01g\x18\x02 \x01(\x05\x12\t\n\x01\x62\x18\x03 \x01(\x05\x12\n\n\x02ww\x18\x04 \x01(\x05\x12\n\n\x02\x63w\x18\x05 \x01(\x05\x12\x0b\n\x03lux\x18\x06 \x01(\x05\x12\x0f\n\x07\x65nabled\x18\x07 \x01(\x08\"]\n\x15LightExitEffectTarget\x12\t\n\x01r\x18\x01 \x01(\x05\x12\t\n\x01g\x18\x02 \x01(\x05\x12\t\n\x01\x62\x18\x03 \x01(\x05\x12\n\n\x02\x63w\x18\x04 \x01(\x05\x12\n\n\x02ww\x18\x05 \x01(\x05\x12\x0b\n\x03lux\x18\x06 \x01(\x05\"l\n\x0fLightExitEffect\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x13\n\x0b\x64uration_ms\x18\x02 \x01(\x05\x12\x36\n\x06target\x18\x03 \x01(\x0b\x32&.bionode.comm.v1.LightExitEffectTarget\"\xcd\x01\n\x0bSchemeLight\x12\x0c\n\x04mode\x18\x01 \x01(\t\x12\x35\n\x0b\x63olor_stops\x18\x02 \x03(\x0b\x32 .bionode.comm.v1.SchemeColorStop\x12\x1c\n\x14transition_durations\x18\x03 \x03(\x05\x12\x0f\n\x07\x65nabled\x18\x04 \x01(\x08\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\x35\n\x0b\x65xit_effect\x18\x06 \x01(\x0b\x32 .bionode.comm.v1.LightExitEffect\"_\n\x0bSchemeTrack\x12\x13\n\x0bmaterial_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x0e\n\x06volume\x18\x04 \x01(\x05\x12\x0f\n\x07\x65nabled\x18\x05 \x01(\x08\"o\n\x0bSchemeSound\x12\x0c\n\x04mode\x18\x01 \x01(\t\x12,\n\x06tracks\x18\x02 \x03(\x0b\x32\x1c.bionode.comm.v1.SchemeTrack\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\"\x83\x01\n\x0fSchemeScentSlot\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07release\x18\x03 \x01(\x05\x12\x10\n\x08interval\x18\x04 \x01(\x05\x12\x12\n\ncycle_mode\x18\x05 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x06 \x01(\x08\x12\x0c\n\x04icon\x18\x07 \x01(\t\"r\n\x0bSchemeScent\x12/\n\x05slots\x18\x01 \x03(\x0b\x32 .bionode.comm.v1.SchemeScentSlot\x12\x0c\n\x04mode\x18\x02 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\"U\n\nSchemeTemp\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12\x0c\n\x04mode\x18\x02 \x01(\t\x12\x13\n\x0btarget_temp\x18\x03 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\"\xe2\x01\n\nSchemeItem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12+\n\x05light\x18\x02 \x01(\x0b\x32\x1c.bionode.comm.v1.SchemeLight\x12+\n\x05sound\x18\x03 \x01(\x0b\x32\x1c.bionode.comm.v1.SchemeSound\x12+\n\x05scent\x18\x04 \x01(\x0b\x32\x1c.bionode.comm.v1.SchemeScent\x12)\n\x04temp\x18\x05 \x01(\x0b\x32\x1b.bionode.comm.v1.SchemeTemp\x12\x14\n\x0c\x64uration_sec\x18\x06 \x01(\x05\"\xc8\x01\n\x06Period\x12\r\n\x05phase\x18\x01 \x01(\t\x12\x12\n\nphase_name\x18\x02 \x01(\t\x12*\n\x06scenes\x18\x03 \x03(\x0b\x32\x1a.bionode.comm.v1.SceneItem\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x14\n\x0c\x64uration_sec\x18\x05 \x01(\x05\x12\x16\n\x0etransition_sec\x18\x06 \x01(\x05\x12,\n\x07schemes\x18\x07 \x03(\x0b\x32\x1b.bionode.comm.v1.SchemeItem\"\x81\x01\n\x0bShareConfig\x12\x1c\n\x14share_title_template\x18\x01 \x01(\t\x12\x1b\n\x13share_desc_template\x18\x02 \x01(\t\x12\x17\n\x0fshare_image_url\x18\x03 \x01(\t\x12\x1e\n\x16share_summary_template\x18\x04 \x01(\t\"\xea\x03\n\x0fPersonalityInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tmhr_codes\x18\x02 \x03(\t\x12\x10\n\x08mhr_name\x18\x03 \x01(\t\x12\x15\n\ridentity_name\x18\x04 \x01(\t\x12\x17\n\x0fstatus_analysis\x18\x05 \x01(\t\x12\r\n\x05title\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x07 \x01(\t\x12\x18\n\x10\x63omment_template\x18\x08 \x01(\t\x12\x18\n\x10\x63haracter_3d_url\x18\t \x01(\t\x12\x18\n\x10population_ratio\x18\n \x01(\x01\x12\x46\n\x16indicator_descriptions\x18\x0b \x01(\x0b\x32&.bionode.comm.v1.IndicatorDescriptions\x12(\n\x07periods\x18\x0c \x03(\x0b\x32\x17.bionode.comm.v1.Period\x12\x32\n\x0cshare_config\x18\r \x01(\x0b\x32\x1c.bionode.comm.v1.ShareConfig\x12\x10\n\x08language\x18\x0e \x01(\t\x12\x12\n\nsort_order\x18\x0f \x01(\x05\x12\x0e\n\x06status\x18\x10 \x01(\x05\x12\x13\n\x0b\x63reate_time\x18\x11 \x01(\t\x12\x13\n\x0bupdate_time\x18\x12 \x01(\t\"8\n\x11GetPersonalityReq\x12\x11\n\tmhr_codes\x18\x01 \x03(\t\x12\x10\n\x08language\x18\x02 \x01(\t\"y\n\x14ListPersonalitiesReq\x12,\n\x04page\x18\x01 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\x12\x10\n\x08language\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12\x11\n\tmhr_codes\x18\x04 \x03(\t\"\xa9\x03\n\x14\x43reatePersonalityReq\x12\x11\n\tmhr_codes\x18\x01 \x03(\t\x12\x10\n\x08mhr_name\x18\x02 \x01(\t\x12\x15\n\ridentity_name\x18\x03 \x01(\t\x12\x17\n\x0fstatus_analysis\x18\x04 \x01(\t\x12\r\n\x05title\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12\x18\n\x10\x63omment_template\x18\x07 \x01(\t\x12\x18\n\x10\x63haracter_3d_url\x18\x08 \x01(\t\x12\x18\n\x10population_ratio\x18\t \x01(\x01\x12\x46\n\x16indicator_descriptions\x18\n \x01(\x0b\x32&.bionode.comm.v1.IndicatorDescriptions\x12(\n\x07periods\x18\x0b \x03(\x0b\x32\x17.bionode.comm.v1.Period\x12\x32\n\x0cshare_config\x18\x0c \x01(\x0b\x32\x1c.bionode.comm.v1.ShareConfig\x12\x10\n\x08language\x18\r \x01(\t\x12\x12\n\nsort_order\x18\x0e \x01(\x05\"\xc5\x03\n\x14UpdatePersonalityReq\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tmhr_codes\x18\x02 \x03(\t\x12\x10\n\x08mhr_name\x18\x03 \x01(\t\x12\x15\n\ridentity_name\x18\x04 \x01(\t\x12\x17\n\x0fstatus_analysis\x18\x05 \x01(\t\x12\r\n\x05title\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x07 \x01(\t\x12\x18\n\x10\x63omment_template\x18\x08 \x01(\t\x12\x18\n\x10\x63haracter_3d_url\x18\t \x01(\t\x12\x18\n\x10population_ratio\x18\n \x01(\x01\x12\x46\n\x16indicator_descriptions\x18\x0b \x01(\x0b\x32&.bionode.comm.v1.IndicatorDescriptions\x12(\n\x07periods\x18\x0c \x03(\x0b\x32\x17.bionode.comm.v1.Period\x12\x32\n\x0cshare_config\x18\r \x01(\x0b\x32\x1c.bionode.comm.v1.ShareConfig\x12\x10\n\x08language\x18\x0e \x01(\t\x12\x12\n\nsort_order\x18\x0f \x01(\x05\x12\x0e\n\x06status\x18\x10 \x01(\x05\"S\n\x1bUpdatePersonalityPeriodsReq\x12\n\n\x02id\x18\x01 \x01(\t\x12(\n\x07periods\x18\x02 \x03(\x0b\x32\x17.bionode.comm.v1.Period\"8\n\x1aUpdatePersonalityStatusReq\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\x05\"G\n\x0ePersonalityRes\x12\x35\n\x0bpersonality\x18\x01 \x01(\x0b\x32 .bionode.comm.v1.PersonalityInfo\"|\n\x12PersonalityListRes\x12\x37\n\rpersonalities\x18\x01 \x03(\x0b\x32 .bionode.comm.v1.PersonalityInfo\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"\x86\x01\n\tShareInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0b\n\x03uid\x18\x02 \x01(\t\x12\x10\n\x08share_id\x18\x03 \x01(\t\x12\x11\n\tresult_id\x18\x04 \x01(\t\x12\x12\n\nshare_type\x18\x05 \x01(\t\x12\x12\n\nshare_page\x18\x06 \x01(\t\x12\x13\n\x0b\x63reate_time\x18\x07 \x01(\t\"X\n\x0e\x43reateShareReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x11\n\tresult_id\x18\x02 \x01(\t\x12\x12\n\nshare_type\x18\x03 \x01(\t\x12\x12\n\nshare_page\x18\x04 \x01(\t\"5\n\x08ShareRes\x12)\n\x05share\x18\x01 \x01(\x0b\x32\x1a.bionode.comm.v1.ShareInfo\"&\n\x12GetShareLandingReq\x12\x10\n\x08share_id\x18\x01 \x01(\t\"\xb9\x01\n\x0fShareLandingRes\x12)\n\x05share\x18\x01 \x01(\x0b\x32\x1a.bionode.comm.v1.ShareInfo\x12\x13\n\x0bsharer_name\x18\x02 \x01(\t\x12/\n\x06result\x18\x03 \x01(\x0b\x32\x1f.bionode.comm.v1.QuizResultInfo\x12\x35\n\x0bpersonality\x18\x04 \x01(\x0b\x32 .bionode.comm.v1.PersonalityInfo\"7\n\x0eRecordVisitReq\x12\x10\n\x08share_id\x18\x01 \x01(\t\x12\x13\n\x0bvisitor_uid\x18\x02 \x01(\t\"\xad\x01\n\x11ShareTrackingItem\x12\x10\n\x08share_id\x18\x01 \x01(\t\x12\x12\n\nsharer_uid\x18\x02 \x01(\t\x12\x17\n\x0fsharer_nickname\x18\x03 \x01(\t\x12\x17\n\x0fsharer_mhr_name\x18\x04 \x01(\t\x12\x13\n\x0bvisit_count\x18\x05 \x01(\x05\x12\x16\n\x0enew_user_count\x18\x06 \x01(\x05\x12\x13\n\x0b\x63reate_time\x18\x07 \x01(\t\"D\n\x14ListShareTrackingReq\x12,\n\x04page\x18\x01 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\"|\n\x14ShareTrackingListRes\x12\x35\n\ttrackings\x18\x01 \x03(\x0b\x32\".bionode.comm.v1.ShareTrackingItem\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"\xc8\x01\n\x11SharerRankingItem\x12\x12\n\nsharer_uid\x18\x01 \x01(\t\x12\x17\n\x0fsharer_nickname\x18\x02 \x01(\t\x12\x15\n\rsharer_avatar\x18\x03 \x01(\t\x12\x10\n\x08mhr_name\x18\x04 \x01(\t\x12\x13\n\x0bshare_count\x18\x05 \x01(\x05\x12\x14\n\x0cnew_uv_count\x18\x06 \x01(\x05\x12\x19\n\x11total_visit_count\x18\x07 \x01(\x05\x12\x17\n\x0flast_share_time\x18\x08 \x01(\t\"\x95\x01\n\x14ListSharerRankingReq\x12,\n\x04page\x18\x01 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\x12\x17\n\x0fsharer_nickname\x18\x02 \x01(\t\x12\x10\n\x08mhr_name\x18\x03 \x01(\t\x12\x12\n\nstart_time\x18\x04 \x01(\t\x12\x10\n\x08\x65nd_time\x18\x05 \x01(\t\"{\n\x14SharerRankingListRes\x12\x34\n\x08rankings\x18\x01 \x03(\x0b\x32\".bionode.comm.v1.SharerRankingItem\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"\xc5\x01\n\tShareItem\x12\n\n\x02id\x18\x01 \x01(\t\x12\x10\n\x08share_id\x18\x02 \x01(\t\x12\x0b\n\x03uid\x18\x03 \x01(\t\x12\x11\n\tresult_id\x18\x04 \x01(\t\x12\x12\n\nshare_type\x18\x05 \x01(\t\x12\x12\n\nshare_page\x18\x06 \x01(\t\x12\x13\n\x0bvisit_count\x18\x07 \x01(\x05\x12\x16\n\x0enew_user_count\x18\x08 \x01(\x05\x12\x13\n\x0b\x63reate_time\x18\t \x01(\t\x12\x10\n\x08mhr_name\x18\n \x01(\t\"P\n\x13ListSharesByUserReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12,\n\x04page\x18\x02 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\"i\n\x0cShareListRes\x12*\n\x06shares\x18\x01 \x03(\x0b\x32\x1a.bionode.comm.v1.ShareItem\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"\x9f\x01\n\x0eShareVisitItem\x12\n\n\x02id\x18\x01 \x01(\t\x12\x10\n\x08share_id\x18\x02 \x01(\t\x12\x13\n\x0bvisitor_uid\x18\x03 \x01(\t\x12\x18\n\x10visitor_nickname\x18\x04 \x01(\t\x12\x16\n\x0evisitor_avatar\x18\x05 \x01(\t\x12\x13\n\x0bis_new_user\x18\x06 \x01(\x08\x12\x13\n\x0b\x63reate_time\x18\x07 \x01(\t\"T\n\x12ListShareVisitsReq\x12\x10\n\x08share_id\x18\x01 \x01(\t\x12,\n\x04page\x18\x02 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\"s\n\x11ShareVisitListRes\x12/\n\x06visits\x18\x01 \x03(\x0b\x32\x1f.bionode.comm.v1.ShareVisitItem\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"_\n\nClientInfo\x12\n\n\x02ua\x18\x01 \x01(\t\x12\x10\n\x08platform\x18\x02 \x01(\t\x12\x0e\n\x06screen\x18\x03 \x01(\t\x12\x0f\n\x07network\x18\x04 \x01(\t\x12\x12\n\nwechat_ver\x18\x05 \x01(\t\"\x89\x02\n\rTrackEventReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0e\n\x06\x61pp_id\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\x12\r\n\x05\x65vent\x18\x04 \x01(\t\x12\x13\n\x0btarget_type\x18\x05 \x01(\t\x12\x11\n\ttarget_id\x18\x06 \x01(\t\x12\x0e\n\x06\x61\x63tion\x18\x07 \x01(\t\x12\r\n\x05label\x18\x08 \x01(\t\x12\r\n\x05value\x18\t \x01(\x01\x12\x0c\n\x04page\x18\n \x01(\t\x12\x10\n\x08referrer\x18\x0b \x01(\t\x12\x10\n\x08metadata\x18\x0c \x01(\t\x12\x30\n\x0b\x63lient_info\x18\r \x01(\x0b\x32\x1b.bionode.comm.v1.ClientInfo\"?\n\rBatchTrackReq\x12.\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x1e.bionode.comm.v1.TrackEventReq\"F\n\x0eGetOverviewReq\x12\x0e\n\x06\x61pp_id\x18\x01 \x01(\t\x12\x12\n\nstart_date\x18\x02 \x01(\t\x12\x10\n\x08\x65nd_date\x18\x03 \x01(\t\"\x87\x01\n\x0bOverviewRes\x12\x14\n\x0cpage_view_uv\x18\x01 \x01(\x03\x12\x18\n\x10quiz_complete_uv\x18\x02 \x01(\x03\x12\x17\n\x0f\x63ompletion_rate\x18\x03 \x01(\x01\x12\x19\n\x11interaction_count\x18\x04 \x01(\x03\x12\x14\n\x0ctotal_events\x18\x05 \x01(\x03\"H\n\x10GetPageViewUVReq\x12\x0e\n\x06\x61pp_id\x18\x01 \x01(\t\x12\x12\n\nstart_date\x18\x02 \x01(\t\x12\x10\n\x08\x65nd_date\x18\x03 \x01(\t\"L\n\x14GetQuizCompleteUVReq\x12\x0e\n\x06\x61pp_id\x18\x01 \x01(\t\x12\x12\n\nstart_date\x18\x02 \x01(\t\x12\x10\n\x08\x65nd_date\x18\x03 \x01(\t\"N\n\x16GetInteractionCountReq\x12\x0e\n\x06\x61pp_id\x18\x01 \x01(\t\x12\x12\n\nstart_date\x18\x02 \x01(\t\x12\x10\n\x08\x65nd_date\x18\x03 \x01(\t\"\xcd\x01\n\x10ListLogEventsReq\x12,\n\x04page\x18\x01 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\x12\x0e\n\x06\x61pp_id\x18\x02 \x01(\t\x12\r\n\x05\x65vent\x18\x03 \x01(\t\x12\x13\n\x0btarget_type\x18\x04 \x01(\t\x12\x11\n\ttarget_id\x18\x05 \x01(\t\x12\x0b\n\x03uid\x18\x06 \x01(\t\x12\x11\n\tpage_path\x18\x07 \x01(\t\x12\x12\n\nstart_date\x18\x08 \x01(\t\x12\x10\n\x08\x65nd_date\x18\t \x01(\t\"\x9d\x02\n\x0cLogEventItem\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0b\n\x03uid\x18\x02 \x01(\t\x12\x0e\n\x06\x61pp_id\x18\x03 \x01(\t\x12\x12\n\nsession_id\x18\x04 \x01(\t\x12\r\n\x05\x65vent\x18\x05 \x01(\t\x12\x13\n\x0btarget_type\x18\x06 \x01(\t\x12\x11\n\ttarget_id\x18\x07 \x01(\t\x12\x0e\n\x06\x61\x63tion\x18\x08 \x01(\t\x12\r\n\x05label\x18\t \x01(\t\x12\r\n\x05value\x18\n \x01(\x01\x12\x0c\n\x04page\x18\x0b \x01(\t\x12\x10\n\x08referrer\x18\x0c \x01(\t\x12\x10\n\x08metadata\x18\r \x01(\t\x12\x13\n\x0b\x63reate_time\x18\x0e \x01(\t\x12\x10\n\x08nickname\x18\x0f \x01(\t\x12\x12\n\navatar_url\x18\x10 \x01(\t\"p\n\x10ListLogEventsRes\x12-\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x1d.bionode.comm.v1.LogEventItem\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"H\n\x10GetEventTrendReq\x12\x0e\n\x06\x61pp_id\x18\x01 \x01(\t\x12\x12\n\nstart_date\x18\x02 \x01(\t\x12\x10\n\x08\x65nd_date\x18\x03 \x01(\t\"@\n\rEventTrendRes\x12\r\n\x05\x64\x61tes\x18\x01 \x03(\t\x12\x0f\n\x07pv_list\x18\x02 \x03(\x03\x12\x0f\n\x07uv_list\x18\x03 \x03(\x03\"*\n\x18GetEventFilterOptionsReq\x12\x0e\n\x06\x61pp_id\x18\x01 \x01(\t\"7\n\x15\x45ventFilterOptionsRes\x12\x0f\n\x07\x61pp_ids\x18\x01 \x03(\t\x12\r\n\x05pages\x18\x02 \x03(\t\"\xf4\x01\n\x19\x41udioMaterialCategoryInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x13\n\x0b\x63reate_time\x18\x04 \x01(\t\x12\x13\n\x0bupdate_time\x18\x05 \x01(\t\x12\x0c\n\x04type\x18\x06 \x01(\t\x12\x0c\n\x04\x63ode\x18\x07 \x01(\t\x12\x0f\n\x07name_en\x18\x08 \x01(\t\x12\x15\n\rparent_tag_id\x18\t \x01(\t\x12\x12\n\ncreated_by\x18\n \x01(\t\x12\x12\n\nupdated_by\x18\x0b \x01(\t\x12\x17\n\x0fparent_tag_name\x18\x0c \x01(\t\"\xaf\x01\n\x1eListAudioMaterialCategoriesReq\x12,\n\x04page\x18\x01 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\x12\x13\n\x06status\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\t\x12\x0c\n\x04\x63ode\x18\x05 \x01(\t\x12\x15\n\rparent_tag_id\x18\x06 \x01(\tB\t\n\x07_status\"\x8d\x01\n\x1c\x41udioMaterialCategoryListRes\x12>\n\ncategories\x18\x01 \x03(\x0b\x32*.bionode.comm.v1.AudioMaterialCategoryInfo\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"X\n\x18\x41udioMaterialCategoryRes\x12<\n\x08\x63\x61tegory\x18\x01 \x01(\x0b\x32*.bionode.comm.v1.AudioMaterialCategoryInfo\"\xaa\x01\n\x1e\x43reateAudioMaterialCategoryReq\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0c\n\x04\x63ode\x18\x03 \x01(\t\x12\x0f\n\x07name_en\x18\x04 \x01(\t\x12\x0e\n\x06status\x18\x05 \x01(\t\x12\x15\n\rparent_tag_id\x18\x06 \x01(\t\x12\x12\n\ncreated_by\x18\x07 \x01(\t\x12\x12\n\nupdated_by\x18\x08 \x01(\t\"\x98\x02\n\x1eUpdateAudioMaterialCategoryReq\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\x04name\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06status\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x11\n\x04type\x18\x04 \x01(\tH\x02\x88\x01\x01\x12\x11\n\x04\x63ode\x18\x05 \x01(\tH\x03\x88\x01\x01\x12\x14\n\x07name_en\x18\x06 \x01(\tH\x04\x88\x01\x01\x12\x1a\n\rparent_tag_id\x18\x07 \x01(\tH\x05\x88\x01\x01\x12\x17\n\nupdated_by\x18\x08 \x01(\tH\x06\x88\x01\x01\x42\x07\n\x05_nameB\t\n\x07_statusB\x07\n\x05_typeB\x07\n\x05_codeB\n\n\x08_name_enB\x10\n\x0e_parent_tag_idB\r\n\x0b_updated_by\"C\n\x15\x41udioMaterialTagValue\x12\x0e\n\x06tag_id\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\"\xd1\x02\n\x10\x41udioMaterialTag\x12\x0e\n\x06tag_id\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x14\n\x07\x65n_name\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x1a\n\rparent_tag_id\x18\x05 \x01(\tH\x01\x88\x01\x01\x12\x1c\n\x0fparent_tag_code\x18\x06 \x01(\tH\x02\x88\x01\x01\x12:\n\x05value\x18\x07 \x01(\x0b\x32&.bionode.comm.v1.AudioMaterialTagValueH\x03\x88\x01\x01\x12\x13\n\x0b\x62\x61nd_values\x18\x08 \x03(\x01\x12\x1e\n\x11relative_loudness\x18\t \x01(\x01H\x04\x88\x01\x01\x42\n\n\x08_en_nameB\x10\n\x0e_parent_tag_idB\x12\n\x10_parent_tag_codeB\x08\n\x06_valueB\x14\n\x12_relative_loudness\"\xcc\x04\n\x11\x41udioMaterialInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x08\x12\x13\n\x0b\x63reate_time\x18\x04 \x01(\t\x12\x13\n\x0bupdate_time\x18\x05 \x01(\t\x12\x12\n\naudio_name\x18\x06 \x01(\t\x12\x11\n\taudio_url\x18\x07 \x01(\t\x12\x16\n\x0eoperation_type\x18\x08 \x01(\x05\x12\x12\n\ncreated_by\x18\t \x01(\t\x12\x12\n\nupdated_by\x18\n \x01(\t\x12;\n\x10sleep_stage_tags\x18\x0b \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12<\n\x11\x63ontent_form_tags\x18\x0c \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12\x39\n\x0emechanism_tags\x18\r \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12\x41\n\x16\x61udio_engineering_tags\x18\x0e \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12<\n\x11medical_risk_tags\x18\x0f \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12>\n\x13\x65vidence_level_tags\x18\x10 \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\"\x81\x01\n\x15ListAudioMaterialsReq\x12,\n\x04page\x18\x01 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\x12\x13\n\x06status\x18\x02 \x01(\x08H\x00\x88\x01\x01\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0c\n\x04tags\x18\x04 \x03(\tB\t\n\x07_status\"|\n\x14\x41udioMaterialListRes\x12\x35\n\tmaterials\x18\x01 \x03(\x0b\x32\".bionode.comm.v1.AudioMaterialInfo\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"H\n\x10\x41udioMaterialRes\x12\x34\n\x08material\x18\x01 \x01(\x0b\x32\".bionode.comm.v1.AudioMaterialInfo\"\x1f\n\x0f\x44istinctTagsRes\x12\x0c\n\x04tags\x18\x01 \x03(\t\"\x9e\x04\n\x16\x43reateAudioMaterialReq\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x12\n\naudio_name\x18\x02 \x01(\t\x12\x11\n\taudio_url\x18\x03 \x01(\t\x12\x16\n\x0eoperation_type\x18\x04 \x01(\x05\x12\x12\n\ncreated_by\x18\x05 \x01(\t\x12\x12\n\nupdated_by\x18\x06 \x01(\t\x12;\n\x10sleep_stage_tags\x18\x07 \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12<\n\x11\x63ontent_form_tags\x18\x08 \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12\x39\n\x0emechanism_tags\x18\t \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12\x41\n\x16\x61udio_engineering_tags\x18\n \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12<\n\x11medical_risk_tags\x18\x0b \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12>\n\x13\x65vidence_level_tags\x18\x0c \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12\x11\n\tembedding\x18\r \x03(\x01\"\xc6\x05\n\x16UpdateAudioMaterialReq\x12\n\n\x02id\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06status\x18\x03 \x01(\x08H\x01\x88\x01\x01\x12\x17\n\naudio_name\x18\x04 \x01(\tH\x02\x88\x01\x01\x12\x16\n\taudio_url\x18\x05 \x01(\tH\x03\x88\x01\x01\x12\x1b\n\x0eoperation_type\x18\x06 \x01(\x05H\x04\x88\x01\x01\x12\x17\n\ncreated_by\x18\x07 \x01(\tH\x05\x88\x01\x01\x12\x17\n\nupdated_by\x18\x08 \x01(\tH\x06\x88\x01\x01\x12;\n\x10sleep_stage_tags\x18\t \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12<\n\x11\x63ontent_form_tags\x18\n \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12\x39\n\x0emechanism_tags\x18\x0b \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12\x41\n\x16\x61udio_engineering_tags\x18\x0c \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12<\n\x11medical_risk_tags\x18\r \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12>\n\x13\x65vidence_level_tags\x18\x0e \x03(\x0b\x32!.bionode.comm.v1.AudioMaterialTag\x12\x11\n\tembedding\x18\x0f \x03(\x01\x42\x0e\n\x0c_descriptionB\t\n\x07_statusB\r\n\x0b_audio_nameB\x0c\n\n_audio_urlB\x11\n\x0f_operation_typeB\r\n\x0b_created_byB\r\n\x0b_updated_by\":\n\x1cUpdateAudioMaterialStatusReq\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\x08\"z\n\x10SomniSetAlarmReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x12\n\nalarm_time\x18\x03 \x01(\t\x12\x1f\n\x17\x65stimated_sleep_minutes\x18\x04 \x01(\x05\x12\x10\n\x08language\x18\x05 \x01(\t\"\\\n\x10SomniSetAlarmRes\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x10\n\x08\x61larm_id\x18\x02 \x01(\t\x12\x0f\n\x07plan_id\x18\x03 \x01(\t\x12\x11\n\tmhr_codes\x18\x04 \x03(\t\"2\n\x0fSomniGetPlanReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"{\n\x0fSomniGetPlanRes\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0f\n\x07plan_id\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12\x10\n\x08subtitle\x18\x04 \x01(\t\x12\x12\n\nis_started\x18\x05 \x01(\x08\x12\x0e\n\x06phases\x18\x06 \x01(\t\":\n\x17SomniMarkPlanStartedReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"W\n\x17SomniMarkPlanStartedRes\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0b\n\x03msg\x18\x02 \x01(\t\x12\x0e\n\x06phases\x18\x03 \x01(\t\x12\x13\n\x0brecord_date\x18\x04 \x01(\t\":\n\x17SomniMarkPlanStoppedReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"2\n\x17SomniMarkPlanStoppedRes\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0b\n\x03msg\x18\x02 \x01(\t\">\n\x17SomniPhaseDurationPatch\x12\r\n\x05phase\x18\x01 \x01(\t\x12\x14\n\x0c\x64uration_sec\x18\x02 \x01(\x05\"\xb7\x01\n\x1fSomniStartSomniDeviceSessionReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x17\n\x0f\x61pp_instance_id\x18\x03 \x01(\t\x12H\n\x16phase_duration_patches\x18\x04 \x03(\x0b\x32(.bionode.comm.v1.SomniPhaseDurationPatch\x12\x10\n\x08language\x18\x05 \x01(\t\"M\n\x1fSomniStartSomniDeviceSessionRes\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0b\n\x03msg\x18\x02 \x01(\t\x12\x11\n\tdevice_id\x18\x03 \x01(\t\"A\n\x1eSomniStopSomniDeviceSessionReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"9\n\x1eSomniStopSomniDeviceSessionRes\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0b\n\x03msg\x18\x02 \x01(\t\":\n\x1eSomniBaselineFusionKeyValInt32\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0b\n\x03val\x18\x02 \x01(\x05\"9\n\x1dSomniBaselineFusionKeyValBool\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0b\n\x03val\x18\x02 \x01(\x08\"n\n\x18SomniBaselineFusionSteps\x12=\n\x04list\x18\x01 \x03(\x0b\x32/.bionode.comm.v1.SomniBaselineFusionKeyValInt32\x12\x13\n\x0btotal_steps\x18\x02 \x01(\x05\"[\n\x1bSomniBaselineFusionCalendar\x12<\n\x04list\x18\x01 \x03(\x0b\x32..bionode.comm.v1.SomniBaselineFusionKeyValBool\"\xb5\x02\n\x1aSomniBaselineFusionWeather\x12\x0f\n\x07\x66x_date\x18\x01 \x01(\t\x12\x10\n\x08temp_max\x18\x02 \x01(\t\x12\x10\n\x08temp_min\x18\x03 \x01(\t\x12\x0c\n\x04icon\x18\x04 \x01(\t\x12\x0e\n\x06precip\x18\x05 \x01(\t\x12\x10\n\x08uv_index\x18\x06 \x01(\t\x12\x10\n\x08humidity\x18\x07 \x01(\t\x12\x10\n\x08pressure\x18\x08 \x01(\t\x12\x0b\n\x03vis\x18\t \x01(\t\x12\x10\n\x08wind_dir\x18\n \x01(\t\x12\x12\n\nwind_scale\x18\x0b \x01(\t\x12\x17\n\x0flight_intensity\x18\x0c \x01(\t\x12\x0b\n\x03\x61qi\x18\r \x01(\t\x12\x14\n\x0c\x61qi_category\x18\x0e \x01(\t\x12\x0f\n\x07sunrise\x18\x0f \x01(\t\x12\x0e\n\x06sunset\x18\x10 \x01(\t\"h\n\x18SomniBaselineFusionScore\x12=\n\x04list\x18\x01 \x03(\x0b\x32/.bionode.comm.v1.SomniBaselineFusionKeyValInt32\x12\r\n\x05today\x18\x02 \x01(\x05\"\x8c\x02\n\x18SomniBaselineFusionDaily\x12\x38\n\x05steps\x18\x01 \x01(\x0b\x32).bionode.comm.v1.SomniBaselineFusionSteps\x12>\n\x08\x63\x61lendar\x18\x02 \x01(\x0b\x32,.bionode.comm.v1.SomniBaselineFusionCalendar\x12<\n\x07weather\x18\x03 \x01(\x0b\x32+.bionode.comm.v1.SomniBaselineFusionWeather\x12\x38\n\x05score\x18\x04 \x01(\x0b\x32).bionode.comm.v1.SomniBaselineFusionScore\"@\n\x11SomniGetFusionReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61te\x18\x02 \x01(\t\x12\x10\n\x08language\x18\x03 \x01(\t\":\n\x11SomniGetFusionRes\x12\x12\n\nai_insight\x18\x01 \x01(\t\x12\x11\n\tschedules\x18\x02 \x01(\t\"h\n\x13SomniGetBaselineReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61te\x18\x02 \x01(\t\x12\x10\n\x08language\x18\x03 \x01(\t\x12\x10\n\x08location\x18\x04 \x01(\t\x12\x12\n\nsession_id\x18\x05 \x01(\t\"\xda\x01\n\x13SomniGetBaselineRes\x12\x12\n\nai_insight\x18\x01 \x01(\t\x12\x14\n\x0c\x64\x61te_strings\x18\x02 \x01(\t\x12\x0f\n\x07records\x18\x03 \x01(\t\x12\x11\n\tschedules\x18\x04 \x01(\t\x12?\n\x0c\x66usion_daily\x18\x05 \x01(\x0b\x32).bionode.comm.v1.SomniBaselineFusionDaily\x12\x18\n\x10quiz_avg_bedtime\x18\x06 \x01(\t\x12\x1a\n\x12quiz_avg_wake_time\x18\x07 \x01(\t\"G\n\x11SomniGetReportReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0brecord_date\x18\x02 \x01(\t\x12\x10\n\x08language\x18\x03 \x01(\t\"8\n\x11SomniGetReportRes\x12\x13\n\x0brecord_date\x18\x01 \x01(\t\x12\x0e\n\x06report\x18\x02 \x01(\t\"C\n SomniSleepPlanetIllustrationItem\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x12\n\nsort_order\x18\x02 \x01(\x05\"\x9d\x01\n\x16SomniSleepPlanetParams\x12\x13\n\x0bplanet_size\x18\x01 \x01(\x01\x12\x12\n\nland_color\x18\x02 \x01(\t\x12\x13\n\x0bwater_color\x18\x03 \x01(\t\x12\x12\n\nring_color\x18\x04 \x01(\t\x12\x13\n\x0bring_radius\x18\x05 \x01(\x01\x12\x1c\n\x14planet_noise_density\x18\x06 \x01(\x01\"?\n\x19SomniSleepPlanetStability\x12\r\n\x05score\x18\x01 \x01(\x05\x12\x13\n\x0bgrade_label\x18\x02 \x01(\t\"\xbc\x01\n\x17SomniSleepPlanetMetrics\x12\x10\n\x08\x64\x65\x65p_min\x18\x01 \x01(\x01\x12\x10\n\x08\x64\x65\x65p_pct\x18\x02 \x01(\x01\x12>\n\nefficiency\x18\x03 \x01(\x0b\x32*.bionode.comm.v1.SomniSleepPlanetStability\x12=\n\tstability\x18\x04 \x01(\x0b\x32*.bionode.comm.v1.SomniSleepPlanetStability\"\x9c\x02\n\x16SomniSleepPlanetDetail\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0brecord_date\x18\x02 \x01(\t\x12\x39\n\x07metrics\x18\x03 \x01(\x0b\x32(.bionode.comm.v1.SomniSleepPlanetMetrics\x12\r\n\x05title\x18\x04 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\x37\n\x06planet\x18\x06 \x01(\x0b\x32\'.bionode.comm.v1.SomniSleepPlanetParams\x12H\n\rillustrations\x18\x07 \x03(\x0b\x32\x31.bionode.comm.v1.SomniSleepPlanetIllustrationItem\":\n\x16SomniGetSleepPlanetReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0brecord_date\x18\x02 \x01(\t\"`\n\x16SomniGetSleepPlanetRes\x12\r\n\x05\x66ound\x18\x01 \x01(\x08\x12\x37\n\x06\x64\x65tail\x18\x02 \x01(\x0b\x32\'.bionode.comm.v1.SomniSleepPlanetDetail\"U\n\x18SomniListSleepPlanetsReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12,\n\x04page\x18\x02 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\"\x83\x01\n\x18SomniListSleepPlanetsRes\x12\x38\n\x07planets\x18\x01 \x03(\x0b\x32\'.bionode.comm.v1.SomniSleepPlanetDetail\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"A\n\x1eSomniValidateMonitorSessionReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\",\n\x1eSomniValidateMonitorSessionRes\x12\n\n\x02ok\x18\x01 \x01(\x08\"<\n\x19SomniGetMonitorPayloadReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\",\n\x19SomniGetMonitorPayloadRes\x12\x0f\n\x07payload\x18\x01 \x01(\t\"B\n\x18SomniGetUidByMhrCodesReq\x12\x13\n\x0bsurvey_code\x18\x01 \x01(\t\x12\x11\n\tmhr_codes\x18\x02 \x03(\t\"\'\n\x18SomniGetUidByMhrCodesRes\x12\x0b\n\x03uid\x18\x01 \x01(\t\"j\n\x19SomniCreateQuizSessionReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0brecord_date\x18\x02 \x01(\t\x12\x16\n\x0equiz_result_id\x18\x03 \x01(\t\x12\x13\n\x0bsurvey_code\x18\x04 \x01(\t\"/\n\x19SomniCreateQuizSessionRes\x12\x12\n\nsession_id\x18\x01 \x01(\t\"F\n\x1eSomniUpsertSomniAppInstUserReq\x12\x17\n\x0f\x61pp_instance_id\x18\x01 \x01(\t\x12\x0b\n\x03uid\x18\x02 \x01(\t\"9\n\x1eSomniUpsertSomniAppInstUserRes\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0b\n\x03msg\x18\x02 \x01(\t\"9\n\x1eSomniDeleteSomniAppInstUserReq\x12\x17\n\x0f\x61pp_instance_id\x18\x01 \x01(\t\"@\n\x16SomniQuizPendingOption\x12\x11\n\toption_id\x18\x01 \x01(\t\x12\x13\n\x0boption_text\x18\x02 \x01(\t\"C\n\x19SomniQuizConfigItemConfig\x12\x0b\n\x03min\x18\x01 \x01(\x05\x12\x0b\n\x03max\x18\x02 \x01(\x05\x12\x0c\n\x04step\x18\x03 \x01(\x05\"\x80\x02\n\"SomniQuizPendingQuestionConfigItem\x12\r\n\x05index\x18\x01 \x01(\x05\x12\r\n\x05label\x18\x02 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x03 \x01(\t\x12\r\n\x05title\x18\x04 \x01(\t\x12\x12\n\ninput_type\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12\x38\n\x07options\x18\x07 \x03(\x0b\x32\'.bionode.comm.v1.SomniQuizPendingOption\x12:\n\x06\x63onfig\x18\x08 \x01(\x0b\x32*.bionode.comm.v1.SomniQuizConfigItemConfig\"d\n\x1eSomniQuizPendingQuestionConfig\x12\x42\n\x05items\x18\x01 \x03(\x0b\x32\x33.bionode.comm.v1.SomniQuizPendingQuestionConfigItem\"\xe8\x01\n\x18SomniQuizPendingQuestion\x12\x13\n\x0bquestion_id\x18\x01 \x01(\t\x12\x0b\n\x03qid\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12\x12\n\ninput_type\x18\x04 \x01(\t\x12\x0c\n\x04tags\x18\x05 \x03(\t\x12\x38\n\x07options\x18\x06 \x03(\x0b\x32\'.bionode.comm.v1.SomniQuizPendingOption\x12?\n\x06\x63onfig\x18\x07 \x01(\x0b\x32/.bionode.comm.v1.SomniQuizPendingQuestionConfig\"]\n\x17SomniQuizDirectAnswerIn\x12\x13\n\x0bquestion_id\x18\x01 \x01(\t\x12\x18\n\x10selected_options\x18\x02 \x03(\t\x12\x13\n\x0binput_value\x18\x03 \x01(\t\"8\n\x16SomniQuizReportSection\x12\r\n\x05title\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\"\x9a\x02\n\x10SomniQuizChatReq\x12\x19\n\x11\x63lient_session_id\x18\x01 \x01(\t\x12\x0e\n\x06resume\x18\x02 \x01(\x08\x12\x13\n\x0bsurvey_code\x18\x03 \x01(\t\x12\x10\n\x08language\x18\x04 \x01(\t\x12\x0f\n\x07message\x18\x05 \x01(\t\x12?\n\rdirect_answer\x18\x06 \x01(\x0b\x32(.bionode.comm.v1.SomniQuizDirectAnswerIn\x12\x17\n\x0f\x61pp_instance_id\x18\x07 \x01(\t\x12\x11\n\tquiz_mode\x18\x08 \x01(\t\x12\x11\n\tcity_name\x18\t \x01(\t\x12\x11\n\tlongitude\x18\n \x01(\x01\x12\x10\n\x08latitude\x18\x0b \x01(\x01\"\xe0\x02\n\x10SomniQuizChatRes\x12\x19\n\x11\x63lient_session_id\x18\x01 \x01(\t\x12\x19\n\x11\x61ssistant_message\x18\x02 \x01(\t\x12\x43\n\x10pending_question\x18\x03 \x01(\x0b\x32).bionode.comm.v1.SomniQuizPendingQuestion\x12\x11\n\tfinalized\x18\x04 \x01(\x08\x12\x12\n\nsession_id\x18\x05 \x01(\t\x12\x0b\n\x03uid\x18\x06 \x01(\t\x12\x12\n\nchat_state\x18\x07 \x01(\x08\x12\x18\n\x10progress_percent\x18\x08 \x01(\x01\x12\x11\n\tmhr_codes\x18\t \x03(\t\x12\x1a\n\x12\x61nswer_status_code\x18\n \x01(\t\x12@\n\x0freport_sections\x18\x0b \x03(\x0b\x32\'.bionode.comm.v1.SomniQuizReportSection\"y\n\x15InterventionCondition\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x0e\n\x06phases\x18\x02 \x03(\t\x12\r\n\x05\x66ield\x18\x03 \x01(\t\x12\x10\n\x08operator\x18\x04 \x01(\t\x12\r\n\x05value\x18\x05 \x01(\x01\x12\x12\n\nbool_value\x18\x06 \x01(\x08\"\x80\x01\n\x14InterventionScenario\x12\x12\n\nlight_json\x18\x01 \x01(\t\x12\x12\n\nsound_json\x18\x02 \x01(\t\x12\x12\n\nscent_json\x18\x03 \x01(\t\x12\x14\n\x0c\x64uration_sec\x18\x04 \x01(\x05\x12\x16\n\x0etransition_sec\x18\x05 \x01(\x05\"\xe7\x02\n\x16InterventionConfigInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x04 \x01(\x08\x12\x10\n\x08priority\x18\x05 \x01(\x05\x12\x14\n\x0c\x63ooldown_sec\x18\x06 \x01(\x05\x12\x17\n\x0f\x63ondition_logic\x18\x07 \x01(\t\x12:\n\nconditions\x18\x08 \x03(\x0b\x32&.bionode.comm.v1.InterventionCondition\x12\x37\n\x08scenario\x18\t \x01(\x0b\x32%.bionode.comm.v1.InterventionScenario\x12\x12\n\nsort_order\x18\n \x01(\x05\x12\x13\n\x0b\x63reate_time\x18\x0b \x01(\t\x12\x13\n\x0bupdate_time\x18\x0c \x01(\t\x12\x0c\n\x04show\x18\r \x01(\x08\x12\x12\n\nevent_type\x18\x0e \x01(\t\"\x1c\n\x1aListInterventionConfigsReq\"R\n\x19InterventionConfigListRes\x12\x35\n\x04list\x18\x01 \x03(\x0b\x32\'.bionode.comm.v1.InterventionConfigInfo\"\xc2\x02\n\x1bUpsertInterventionConfigReq\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x04 \x01(\x08\x12\x10\n\x08priority\x18\x05 \x01(\x05\x12\x14\n\x0c\x63ooldown_sec\x18\x06 \x01(\x05\x12\x17\n\x0f\x63ondition_logic\x18\x07 \x01(\t\x12:\n\nconditions\x18\x08 \x03(\x0b\x32&.bionode.comm.v1.InterventionCondition\x12\x37\n\x08scenario\x18\t \x01(\x0b\x32%.bionode.comm.v1.InterventionScenario\x12\x12\n\nsort_order\x18\n \x01(\x05\x12\x0c\n\x04show\x18\x0b \x01(\x08\x12\x12\n\nevent_type\x18\x0c \x01(\t\"P\n\x15InterventionConfigRes\x12\x37\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\'.bionode.comm.v1.InterventionConfigInfo\":\n\x16TriggerInterventionReq\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\")\n\x13StopInterventionReq\x12\x12\n\nsession_id\x18\x01 \x01(\t\"5\n\x1fResetAiThoughtRehearsalStateReq\x12\x12\n\nsession_id\x18\x01 \x01(\t\"O\n\x1a\x45mitDeviceLinkAiThoughtReq\x12\x11\n\tdevice_id\x18\x01 \x01(\t\x12\x0e\n\x06online\x18\x02 \x01(\x08\x12\x0e\n\x06reason\x18\x03 \x01(\t\"+\n\x15SomniGetPlanPhasesReq\x12\x12\n\nsession_id\x18\x01 \x01(\t\"\xbc\x01\n\x12SomniPlanPhaseItem\x12\r\n\x05phase\x18\x01 \x01(\t\x12\x12\n\nphase_name\x18\x02 \x01(\t\x12\x18\n\x10\x64uration_minutes\x18\x03 \x01(\x05\x12\x19\n\x11light_description\x18\x04 \x01(\t\x12\x19\n\x11sound_description\x18\x05 \x01(\t\x12\x19\n\x11scent_description\x18\x06 \x01(\t\x12\x18\n\x10temp_description\x18\x07 \x01(\t\"L\n\x15SomniGetPlanPhasesRes\x12\x33\n\x06phases\x18\x01 \x03(\x0b\x32#.bionode.comm.v1.SomniPlanPhaseItem\"3\n\x1dSomniGetInterventionEventsReq\x12\x12\n\nsession_id\x18\x01 \x01(\t\"\xcb\x01\n\x1aSomniInterventionEventItem\x12\x0c\n\x04time\x18\x01 \x01(\t\x12\x12\n\nevent_type\x18\x02 \x01(\t\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x0c\n\x04\x63ode\x18\x04 \x01(\t\x12\x15\n\rtrigger_cause\x18\x05 \x01(\t\x12\x14\n\x0c\x61\x63tion_taken\x18\x06 \x01(\t\x12\x16\n\x0eresult_summary\x18\x07 \x01(\t\x12\x18\n\x10related_event_id\x18\x08 \x01(\t\x12\x10\n\x08\x64uration\x18\t \x01(\t\"\\\n\x1dSomniGetInterventionEventsRes\x12;\n\x06\x65vents\x18\x01 \x03(\x0b\x32+.bionode.comm.v1.SomniInterventionEventItem\"G\n\x1cSomniGetMonitorReportDataReq\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x61ta_source\x18\x02 \x01(\t\"7\n!SomniGenerateMonitorDphReportsReq\x12\x12\n\nsession_id\x18\x01 \x01(\t\"C\n!SomniGenerateMonitorDphReportsRes\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"i\n\x16PhysiologicalDataPoint\x12\x0c\n\x04time\x18\x01 \x01(\t\x12\x12\n\nheart_rate\x18\x02 \x01(\x01\x12\x18\n\x10respiration_rate\x18\x03 \x01(\x01\x12\x13\n\x0b\x62ody_motion\x18\x04 \x01(\x01\"o\n\x14\x45nvironmentDataPoint\x12\x0c\n\x04time\x18\x01 \x01(\t\x12\x13\n\x0btemperature\x18\x02 \x01(\x01\x12\x10\n\x08humidity\x18\x03 \x01(\x01\x12\x13\n\x0billuminance\x18\x04 \x01(\x01\x12\r\n\x05noise\x18\x05 \x01(\x01\"g\n\x13InterventionSummary\x12\x13\n\x0btotal_count\x18\x01 \x01(\x05\x12;\n\x06\x65vents\x18\x02 \x03(\x0b\x32+.bionode.comm.v1.SomniInterventionEventItem\"T\n\x14PersonalityHighlight\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0f\n\x07subhead\x18\x03 \x01(\t\x12\x0f\n\x07\x64\x65tails\x18\x04 \x01(\t\"\x99\x01\n\x0cRadarMetrics\x12\x0c\n\x04time\x18\x01 \x01(\t\x12\x18\n\x10T_resistance_sec\x18\x02 \x01(\x05\x12\x12\n\nHR_initial\x18\x03 \x01(\x01\x12\x11\n\tHR_stable\x18\x04 \x01(\x01\x12\x12\n\nRR_initial\x18\x05 \x01(\x01\x12\x11\n\tRR_stable\x18\x06 \x01(\x01\x12\x13\n\x0b\x62ody_motion\x18\x07 \x01(\x01\"\x87\x01\n\x0bRadarResult\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x38\n\thighlight\x18\x02 \x01(\x0b\x32%.bionode.comm.v1.PersonalityHighlight\x12.\n\x07metrics\x18\x03 \x01(\x0b\x32\x1d.bionode.comm.v1.RadarMetrics\"u\n\x0e\x41nomalySegment\x12\x10\n\x08start_ts\x18\x01 \x01(\x03\x12\x0e\n\x06\x65nd_ts\x18\x02 \x01(\x03\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\r\n\x05label\x18\x04 \x01(\t\x12\x12\n\nstart_time\x18\x05 \x01(\t\x12\x10\n\x08\x65nd_time\x18\x06 \x01(\t\"\x95\x01\n\rAnomalyStatus\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\r\n\x05level\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x19\n\x11session_completed\x18\x04 \x01(\x08\x12\x39\n\x10\x61nomaly_segments\x18\x05 \x03(\x0b\x32\x1f.bionode.comm.v1.AnomalySegment\"4\n\x15\x45nvironmentCardSeries\x12\x0e\n\x06series\x18\x01 \x03(\x01\x12\x0b\n\x03\x61vg\x18\x02 \x01(\x01\"\x84\x01\n\x0b\x43limateCard\x12;\n\x0btemperature\x18\x01 \x01(\x0b\x32&.bionode.comm.v1.EnvironmentCardSeries\x12\x38\n\x08humidity\x18\x02 \x01(\x0b\x32&.bionode.comm.v1.EnvironmentCardSeries\"\x83\x02\n\x10\x45nvironmentCards\x12-\n\x07\x63limate\x18\x01 \x01(\x0b\x32\x1c.bionode.comm.v1.ClimateCard\x12=\n\rambient_light\x18\x02 \x01(\x0b\x32&.bionode.comm.v1.EnvironmentCardSeries\x12@\n\x10\x61\x63oustic_masking\x18\x03 \x01(\x0b\x32&.bionode.comm.v1.EnvironmentCardSeries\x12?\n\x0f\x61ir_conditioner\x18\x04 \x01(\x0b\x32&.bionode.comm.v1.EnvironmentCardSeries\"2\n\x0fPlanOverviewRgb\x12\t\n\x01r\x18\x01 \x01(\x05\x12\t\n\x01g\x18\x02 \x01(\x05\x12\t\n\x01\x62\x18\x03 \x01(\x05\"5\n\x11PlanOverviewScent\x12\x12\n\nscent_icon\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\"^\n\x16PlanOverviewLightPhase\x12\r\n\x05phase\x18\x01 \x01(\t\x12\x35\n\x0b\x63olor_stops\x18\x02 \x03(\x0b\x32 .bionode.comm.v1.PlanOverviewRgb\"\xa9\x01\n\rPlanOverviews\x12\x31\n\x05scent\x18\x02 \x03(\x0b\x32\".bionode.comm.v1.PlanOverviewScent\x12\r\n\x05sound\x18\x03 \x03(\t\x12=\n\x0clight_phases\x18\x04 \x03(\x0b\x32\'.bionode.comm.v1.PlanOverviewLightPhaseJ\x04\x08\x01\x10\x02R\x11light_color_stops\"J\n!SomniQuizUserProfileSleepWakeTime\x12\x12\n\nsleep_time\x18\x01 \x01(\t\x12\x11\n\twake_time\x18\x02 \x01(\t\"\xa8\x01\n\x14SomniQuizUserProfile\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x15\n\rdaytime_event\x18\x02 \x01(\t\x12\x18\n\x10improvement_goal\x18\x03 \x01(\t\x12Q\n\x15usual_sleep_wake_time\x18\x04 \x01(\x0b\x32\x32.bionode.comm.v1.SomniQuizUserProfileSleepWakeTime\"\x8b\x04\n\x1cSomniGetMonitorReportDataRes\x12\x45\n\x14physiological_series\x18\x01 \x03(\x0b\x32\'.bionode.comm.v1.PhysiologicalDataPoint\x12\x41\n\x12\x65nvironment_series\x18\x02 \x03(\x0b\x32%.bionode.comm.v1.EnvironmentDataPoint\x12\x42\n\x14intervention_summary\x18\x03 \x01(\x0b\x32$.bionode.comm.v1.InterventionSummary\x12\x32\n\x0cradar_result\x18\x04 \x01(\x0b\x32\x1c.bionode.comm.v1.RadarResult\x12\x36\n\x0e\x61nomaly_status\x18\x05 \x01(\x0b\x32\x1e.bionode.comm.v1.AnomalyStatus\x12<\n\x11\x65nvironment_cards\x18\x06 \x01(\x0b\x32!.bionode.comm.v1.EnvironmentCards\x12\x36\n\x0eplan_overviews\x18\x07 \x01(\x0b\x32\x1e.bionode.comm.v1.PlanOverviews\x12;\n\x0cuser_profile\x18\x08 \x01(\x0b\x32%.bionode.comm.v1.SomniQuizUserProfile\"/\n\x19SomniGetSessionContextReq\x12\x12\n\nsession_id\x18\x01 \x01(\t\"\xb4\x01\n\x1cSomniSessionContextPlanPhase\x12\r\n\x05phase\x18\x01 \x01(\t\x12\x12\n\nphase_name\x18\x02 \x01(\t\x12\x14\n\x0c\x64uration_sec\x18\x03 \x01(\x05\x12\x16\n\x0etransition_sec\x18\x04 \x01(\x05\x12\x18\n\x10\x64uration_minutes\x18\x05 \x01(\x05\x12\x14\n\x0cschemes_json\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x07 \x01(\t\"\x87\x01\n\x19SomniGetSessionContextRes\x12\x11\n\tmhr_codes\x18\x01 \x03(\t\x12\x42\n\x0bplan_phases\x18\x02 \x03(\x0b\x32-.bionode.comm.v1.SomniSessionContextPlanPhase\x12\x13\n\x0bsurvey_code\x18\x03 \x01(\t\"6\n SomniConsoleStopDeviceSessionReq\x12\x12\n\nsession_id\x18\x01 \x01(\t\";\n SomniConsoleStopDeviceSessionRes\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0b\n\x03msg\x18\x02 \x01(\t\"\xb0\x01\n\x0f\x43hatMessageInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x17\n\x0f\x63onversation_id\x18\x02 \x01(\t\x12\x10\n\x08\x62iz_type\x18\x03 \x01(\t\x12\x15\n\rmessage_index\x18\x04 \x01(\x05\x12\x0c\n\x04role\x18\x05 \x01(\t\x12\x14\n\x0c\x63ontent_json\x18\x06 \x01(\t\x12\x16\n\x0emeta_data_json\x18\x07 \x01(\t\x12\x13\n\x0b\x63reate_time\x18\x08 \x01(\t\"-\n\x12GetChatMessagesReq\x12\x17\n\x0f\x63onversation_id\x18\x01 \x01(\t\"D\n\x12\x43hatMessageListRes\x12.\n\x04list\x18\x01 \x03(\x0b\x32 .bionode.comm.v1.ChatMessageInfo\"\x8f\x01\n\x14\x43hatConversationItem\x12\x17\n\x0f\x63onversation_id\x18\x01 \x01(\t\x12\x10\n\x08\x62iz_type\x18\x02 \x01(\t\x12\x15\n\rmessage_count\x18\x03 \x01(\x05\x12\x1a\n\x12\x66irst_message_time\x18\x04 \x01(\t\x12\x19\n\x11last_message_time\x18\x05 \x01(\t\"\x80\x01\n\x18ListChatConversationsReq\x12\x10\n\x08\x62iz_type\x18\x01 \x01(\t\x12\x12\n\nstart_time\x18\x02 \x01(\t\x12\x10\n\x08\x65nd_time\x18\x03 \x01(\t\x12,\n\x04page\x18\x04 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\"k\n\x1cListUserChatConversationsReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x10\n\x08\x62iz_type\x18\x02 \x01(\t\x12,\n\x04page\x18\x03 \x01(\x0b\x32\x1e.bionode.common.v1.PageRequest\"\x86\x01\n\x17\x43hatConversationListRes\x12<\n\rconversations\x18\x01 \x03(\x0b\x32%.bionode.comm.v1.ChatConversationItem\x12-\n\x04page\x18\x02 \x01(\x0b\x32\x1f.bionode.common.v1.PageResponse\"H\n\x13SomniGetTempPlanReq\x12\x11\n\tmhr_codes\x18\x01 \x03(\t\x12\x10\n\x08language\x18\x02 \x01(\t\x12\x0c\n\x04type\x18\x03 \x01(\t\"U\n\x13SomniGetTempPlanRes\x12\r\n\x05\x66ound\x18\x01 \x01(\x08\x12\r\n\x05title\x18\x02 \x01(\t\x12\x10\n\x08subtitle\x18\x03 \x01(\t\x12\x0e\n\x06phases\x18\x04 \x01(\t\"8\n\x12UploadChatAudioReq\x12\x12\n\naudio_data\x18\x01 \x01(\x0c\x12\x0e\n\x06prefix\x18\x02 \x01(\t\"\'\n\x12UploadChatAudioRes\x12\x11\n\taudio_url\x18\x01 \x01(\t\"}\n\x14\x41ppendChatMessageReq\x12\x17\n\x0f\x63onversation_id\x18\x01 \x01(\t\x12\x10\n\x08\x62iz_type\x18\x02 \x01(\t\x12\x0c\n\x04role\x18\x03 \x01(\t\x12\x14\n\x0c\x63ontent_json\x18\x04 \x01(\t\x12\x16\n\x0emeta_data_json\x18\x05 \x01(\t\"\x9b\x01\n\x13SomniSleepMapRegion\x12\x15\n\rprovince_code\x18\x01 \x01(\t\x12\x11\n\tcity_code\x18\x02 \x01(\t\x12\x15\n\rdistrict_code\x18\x03 \x01(\t\x12\x10\n\x08province\x18\x04 \x01(\t\x12\x0c\n\x04\x63ity\x18\x05 \x01(\t\x12\x10\n\x08\x64istrict\x18\x06 \x01(\t\x12\x11\n\tfull_path\x18\x07 \x01(\t\"z\n\x1aSomniSleepMapDimensionGrpc\x12\r\n\x05score\x18\x01 \x01(\x01\x12\x0e\n\x06weight\x18\x02 \x01(\x01\x12\x12\n\nvalue_json\x18\x03 \x01(\t\x12\x15\n\rcity_avg_json\x18\x04 \x01(\t\x12\x12\n\ncity_score\x18\x05 \x01(\x01\"\xf9\x02\n\x1bSomniSleepMapDimensionsGrpc\x12?\n\ndeep_sleep\x18\x01 \x01(\x0b\x32+.bionode.comm.v1.SomniSleepMapDimensionGrpc\x12\x43\n\x0esleep_duration\x18\x02 \x01(\x0b\x32+.bionode.comm.v1.SomniSleepMapDimensionGrpc\x12\x45\n\x10sleep_efficiency\x18\x03 \x01(\x0b\x32+.bionode.comm.v1.SomniSleepMapDimensionGrpc\x12\x44\n\x0f\x61\x62normal_events\x18\x04 \x01(\x0b\x32+.bionode.comm.v1.SomniSleepMapDimensionGrpc\x12G\n\x12routine_regularity\x18\x05 \x01(\x0b\x32+.bionode.comm.v1.SomniSleepMapDimensionGrpc\"\xde\x02\n\x19SomniSleepMapAnalysisGrpc\x12\x0b\n\x03\x61id\x18\x01 \x01(\t\x12\x0b\n\x03uid\x18\x02 \x01(\t\x12\x12\n\nstats_date\x18\x03 \x01(\t\x12\x34\n\x06region\x18\x04 \x01(\x0b\x32$.bionode.comm.v1.SomniSleepMapRegion\x12\x11\n\tuser_name\x18\x05 \x01(\t\x12\r\n\x05score\x18\x06 \x01(\x01\x12\x15\n\rsleep_seconds\x18\x07 \x01(\x01\x12\x1a\n\x12\x64\x65\x65p_sleep_seconds\x18\x08 \x01(\x01\x12\x18\n\x10\x64\x65\x65p_sleep_ratio\x18\t \x01(\x01\x12\x12\n\nevaluation\x18\n \x01(\t\x12@\n\ndimensions\x18\x0b \x01(\x0b\x32,.bionode.comm.v1.SomniSleepMapDimensionsGrpc\x12\x18\n\x10is_env_sensitive\x18\x0c \x01(\x08\"\xc4\x01\n\x19SomniSleepMapDistrictGrpc\x12\x34\n\x06region\x18\x01 \x01(\x0b\x32$.bionode.comm.v1.SomniSleepMapRegion\x12\r\n\x05score\x18\x02 \x01(\x01\x12\x15\n\rsleep_seconds\x18\x03 \x01(\x01\x12\x18\n\x10\x64\x65\x65p_sleep_ratio\x18\x04 \x01(\x01\x12\x1c\n\x14sensitive_user_ratio\x18\x05 \x01(\x01\x12\x13\n\x0bheatmap_url\x18\x06 \x01(\t\"\xae\x01\n\x1cSomniSleepMapCitySummaryGrpc\x12\x34\n\x06region\x18\x01 \x01(\x0b\x32$.bionode.comm.v1.SomniSleepMapRegion\x12\x1f\n\x17\x61vg_comprehensive_score\x18\x02 \x01(\x01\x12\x18\n\x10score_beat_count\x18\x03 \x01(\x05\x12\x1d\n\x15\x64\x65\x65p_ratio_beat_count\x18\x04 \x01(\x05\"\x95\x01\n\x1fSomniGetSleepMapCityOverviewReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x12\n\nstats_date\x18\x02 \x01(\t\x12\x10\n\x08language\x18\x03 \x01(\t\x12\x15\n\rprovince_code\x18\x04 \x01(\t\x12\x11\n\tcity_code\x18\x05 \x01(\t\x12\x15\n\rdistrict_code\x18\x06 \x01(\t\"\xdc\x01\n\x1fSomniGetSleepMapCityOverviewRes\x12\x12\n\nstats_date\x18\x01 \x01(\t\x12\x1c\n\x14\x66ound_focus_district\x18\x02 \x01(\x08\x12\x42\n\x0e\x66ocus_district\x18\x03 \x01(\x0b\x32*.bionode.comm.v1.SomniSleepMapDistrictGrpc\x12\x43\n\x0c\x63ity_summary\x18\x04 \x01(\x0b\x32-.bionode.comm.v1.SomniSleepMapCitySummaryGrpc\"_\n\x1dSomniGetSleepMapHighlightsReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61id\x18\x02 \x01(\t\x12\x12\n\nstats_date\x18\x03 \x01(\t\x12\x10\n\x08language\x18\x04 \x01(\t\"\xa4\x01\n\x1dSomniGetSleepMapHighlightsRes\x12\r\n\x05\x66ound\x18\x01 \x01(\x08\x12<\n\x08\x61nalysis\x18\x02 \x01(\x0b\x32*.bionode.comm.v1.SomniSleepMapAnalysisGrpc\x12\x11\n\tcity_rank\x18\x03 \x01(\x05\x12\x12\n\nbeat_ratio\x18\x04 \x01(\x05\x12\x0f\n\x07is_self\x18\x05 \x01(\x08\"\xaf\x01\n\x1fSomniSleepMapLeaderboardRowGrpc\x12\x0c\n\x04rank\x18\x01 \x01(\x05\x12\x0b\n\x03uid\x18\x02 \x01(\t\x12\x0b\n\x03\x61id\x18\x03 \x01(\t\x12\x11\n\tuser_name\x18\x04 \x01(\t\x12\x1a\n\x12\x64\x65\x65p_sleep_seconds\x18\x05 \x01(\x01\x12\x15\n\rsleep_seconds\x18\x06 \x01(\x01\x12\r\n\x05score\x18\x07 \x01(\x01\x12\x0f\n\x07is_self\x18\x08 \x01(\x08\"\x9b\x01\n#SomniSleepMapCurrentUserSummaryGrpc\x12\r\n\x05score\x18\x01 \x01(\x01\x12\x1a\n\x12\x64\x65\x65p_sleep_seconds\x18\x02 \x01(\x01\x12\x15\n\rsleep_seconds\x18\x03 \x01(\x01\x12\x12\n\nevaluation\x18\x04 \x01(\t\x12\x11\n\tcity_rank\x18\x05 \x01(\x05\x12\x0b\n\x03\x61id\x18\x06 \x01(\t\"b\n\x1eSomniGetSleepMapCityRankingReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x12\n\nstats_date\x18\x02 \x01(\t\x12\r\n\x05limit\x18\x03 \x01(\x05\x12\x10\n\x08language\x18\x04 \x01(\t\"\x8c\x02\n\x1eSomniGetSleepMapCityRankingRes\x12\x12\n\nstats_date\x18\x01 \x01(\t\x12;\n\rfilter_region\x18\x02 \x01(\x0b\x32$.bionode.comm.v1.SomniSleepMapRegion\x12\x45\n\x0bleaderboard\x18\x03 \x03(\x0b\x32\x30.bionode.comm.v1.SomniSleepMapLeaderboardRowGrpc\x12R\n\x14\x63urrent_user_summary\x18\x04 \x01(\x0b\x32\x34.bionode.comm.v1.SomniSleepMapCurrentUserSummaryGrpc\"f\n\x1a\x43reateDphConsoleSessionReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x11\n\tscale_uid\x18\x02 \x01(\t\x12\x13\n\x0brecord_date\x18\x03 \x01(\t\x12\x13\n\x0bphases_json\x18\x04 \x01(\t\"0\n\x1a\x43reateDphConsoleSessionRes\x12\x12\n\nsession_id\x18\x01 \x01(\t2\xd5\x0b\n\x0bQuizService\x12^\n\x12GetActiveQuestions\x12&.bionode.comm.v1.GetActiveQuestionsReq\x1a .bionode.comm.v1.QuestionListRes\x12T\n\rListQuestions\x12!.bionode.comm.v1.ListQuestionsReq\x1a .bionode.comm.v1.QuestionListRes\x12\x43\n\x0bGetQuestion\x12\x16.bionode.comm.v1.IdReq\x1a\x1c.bionode.comm.v1.QuestionRes\x12R\n\x0e\x43reateQuestion\x12\".bionode.comm.v1.CreateQuestionReq\x1a\x1c.bionode.comm.v1.QuestionRes\x12R\n\x0eUpdateQuestion\x12\".bionode.comm.v1.UpdateQuestionReq\x1a\x1c.bionode.comm.v1.QuestionRes\x12[\n\x14UpdateQuestionStatus\x12(.bionode.comm.v1.UpdateQuestionStatusReq\x1a\x19.bionode.comm.v1.EmptyRes\x12\x43\n\x0e\x44\x65leteQuestion\x12\x16.bionode.comm.v1.IdReq\x1a\x19.bionode.comm.v1.EmptyRes\x12`\n\x11GetQuestionsByIds\x12).bionode.comm.v1.GetQuestionsByIdsRequest\x1a .bionode.comm.v1.QuestionListRes\x12U\n\x0eGetPersonality\x12\".bionode.comm.v1.GetPersonalityReq\x1a\x1f.bionode.comm.v1.PersonalityRes\x12M\n\x12GetPersonalityById\x12\x16.bionode.comm.v1.IdReq\x1a\x1f.bionode.comm.v1.PersonalityRes\x12_\n\x11ListPersonalities\x12%.bionode.comm.v1.ListPersonalitiesReq\x1a#.bionode.comm.v1.PersonalityListRes\x12[\n\x11\x43reatePersonality\x12%.bionode.comm.v1.CreatePersonalityReq\x1a\x1f.bionode.comm.v1.PersonalityRes\x12[\n\x11UpdatePersonality\x12%.bionode.comm.v1.UpdatePersonalityReq\x1a\x1f.bionode.comm.v1.PersonalityRes\x12\x61\n\x17UpdatePersonalityStatus\x12+.bionode.comm.v1.UpdatePersonalityStatusReq\x1a\x19.bionode.comm.v1.EmptyRes\x12\x63\n\x18UpdatePersonalityPeriods\x12,.bionode.comm.v1.UpdatePersonalityPeriodsReq\x1a\x19.bionode.comm.v1.EmptyRes\x12\x46\n\x11\x44\x65letePersonality\x12\x16.bionode.comm.v1.IdReq\x1a\x19.bionode.comm.v1.EmptyRes\x12N\n\x13GetDistinctMhrCodes\x12\x19.bionode.comm.v1.EmptyRes\x1a\x1c.bionode.comm.v1.MhrCodesRes2\x80\r\n\rSurveyService\x12\x46\n\tGetSurvey\x12\x1d.bionode.comm.v1.GetSurveyReq\x1a\x1a.bionode.comm.v1.SurveyRes\x12R\n\x0fGetSurveyByCode\x12#.bionode.comm.v1.GetSurveyByCodeReq\x1a\x1a.bionode.comm.v1.SurveyRes\x12N\n\x0bListSurveys\x12\x1f.bionode.comm.v1.ListSurveysReq\x1a\x1e.bionode.comm.v1.SurveyListRes\x12L\n\x0c\x43reateSurvey\x12 .bionode.comm.v1.CreateSurveyReq\x1a\x1a.bionode.comm.v1.SurveyRes\x12L\n\x0cUpdateSurvey\x12 .bionode.comm.v1.UpdateSurveyReq\x1a\x1a.bionode.comm.v1.SurveyRes\x12W\n\x12UpdateSurveyStatus\x12&.bionode.comm.v1.UpdateSurveyStatusReq\x1a\x19.bionode.comm.v1.EmptyRes\x12\x41\n\x0c\x44\x65leteSurvey\x12\x16.bionode.comm.v1.IdReq\x1a\x19.bionode.comm.v1.EmptyRes\x12\x66\n\x16GetSurveyWithQuestions\x12#.bionode.comm.v1.GetSurveyByCodeReq\x1a\'.bionode.comm.v1.SurveyWithQuestionsRes\x12H\n\nSaveAnswer\x12\x1e.bionode.comm.v1.SaveAnswerReq\x1a\x1a.bionode.comm.v1.AnswerRes\x12N\n\x10SaveAnswerAlways\x12\x1e.bionode.comm.v1.SaveAnswerReq\x1a\x1a.bionode.comm.v1.AnswerRes\x12V\n\x11GetAnswerProgress\x12%.bionode.comm.v1.GetAnswerProgressReq\x1a\x1a.bionode.comm.v1.AnswerRes\x12N\n\x0bListAnswers\x12\x1f.bionode.comm.v1.ListAnswersReq\x1a\x1e.bionode.comm.v1.AnswerListRes\x12N\n\rGetAnswerById\x12!.bionode.comm.v1.GetAnswerByIdReq\x1a\x1a.bionode.comm.v1.AnswerRes\x12[\n\x0f\x43omputeMhrCodes\x12#.bionode.comm.v1.ComputeMhrCodesReq\x1a#.bionode.comm.v1.ComputeMhrCodesRes\x12L\n\nSubmitQuiz\x12\x1e.bionode.comm.v1.SubmitQuizReq\x1a\x1e.bionode.comm.v1.QuizResultRes\x12N\n\x0cH5SubmitQuiz\x12\x1e.bionode.comm.v1.SubmitQuizReq\x1a\x1e.bionode.comm.v1.QuizResultRes\x12V\n\x0fSubmitScaleQuiz\x12#.bionode.comm.v1.SubmitScaleQuizReq\x1a\x1e.bionode.comm.v1.QuizResultRes\x12V\n\x0fGetLatestResult\x12#.bionode.comm.v1.GetLatestResultReq\x1a\x1e.bionode.comm.v1.QuizResultRes\x12R\n\rGetResultById\x12!.bionode.comm.v1.GetResultByIdReq\x1a\x1e.bionode.comm.v1.QuizResultRes\x12R\n\x0bListResults\x12\x1f.bionode.comm.v1.ListResultsReq\x1a\".bionode.comm.v1.QuizResultListRes2\xf9\x04\n\x0cShareService\x12I\n\x0b\x43reateShare\x12\x1f.bionode.comm.v1.CreateShareReq\x1a\x19.bionode.comm.v1.ShareRes\x12X\n\x0fGetShareLanding\x12#.bionode.comm.v1.GetShareLandingReq\x1a .bionode.comm.v1.ShareLandingRes\x12I\n\x0bRecordVisit\x12\x1f.bionode.comm.v1.RecordVisitReq\x1a\x19.bionode.comm.v1.EmptyRes\x12\x61\n\x11ListShareTracking\x12%.bionode.comm.v1.ListShareTrackingReq\x1a%.bionode.comm.v1.ShareTrackingListRes\x12\x61\n\x11ListSharerRanking\x12%.bionode.comm.v1.ListSharerRankingReq\x1a%.bionode.comm.v1.SharerRankingListRes\x12W\n\x10ListSharesByUser\x12$.bionode.comm.v1.ListSharesByUserReq\x1a\x1d.bionode.comm.v1.ShareListRes\x12Z\n\x0fListShareVisits\x12#.bionode.comm.v1.ListShareVisitsReq\x1a\".bionode.comm.v1.ShareVisitListRes2\x86\x06\n\x0c\x45ventService\x12G\n\nTrackEvent\x12\x1e.bionode.comm.v1.TrackEventReq\x1a\x19.bionode.comm.v1.EmptyRes\x12G\n\nBatchTrack\x12\x1e.bionode.comm.v1.BatchTrackReq\x1a\x19.bionode.comm.v1.EmptyRes\x12L\n\x0bGetOverview\x12\x1f.bionode.comm.v1.GetOverviewReq\x1a\x1c.bionode.comm.v1.OverviewRes\x12M\n\rGetPageViewUV\x12!.bionode.comm.v1.GetPageViewUVReq\x1a\x19.bionode.comm.v1.CountRes\x12U\n\x11GetQuizCompleteUV\x12%.bionode.comm.v1.GetQuizCompleteUVReq\x1a\x19.bionode.comm.v1.CountRes\x12Y\n\x13GetInteractionCount\x12\'.bionode.comm.v1.GetInteractionCountReq\x1a\x19.bionode.comm.v1.CountRes\x12U\n\rListLogEvents\x12!.bionode.comm.v1.ListLogEventsReq\x1a!.bionode.comm.v1.ListLogEventsRes\x12R\n\rGetEventTrend\x12!.bionode.comm.v1.GetEventTrendReq\x1a\x1e.bionode.comm.v1.EventTrendRes\x12j\n\x15GetEventFilterOptions\x12).bionode.comm.v1.GetEventFilterOptionsReq\x1a&.bionode.comm.v1.EventFilterOptionsRes2\xe7\"\n\x0cSomniService\x12P\n\x08SetAlarm\x12!.bionode.comm.v1.SomniSetAlarmReq\x1a!.bionode.comm.v1.SomniSetAlarmRes\x12M\n\x07GetPlan\x12 .bionode.comm.v1.SomniGetPlanReq\x1a .bionode.comm.v1.SomniGetPlanRes\x12\x65\n\x0fMarkPlanStarted\x12(.bionode.comm.v1.SomniMarkPlanStartedReq\x1a(.bionode.comm.v1.SomniMarkPlanStartedRes\x12\x65\n\x0fMarkPlanStopped\x12(.bionode.comm.v1.SomniMarkPlanStoppedReq\x1a(.bionode.comm.v1.SomniMarkPlanStoppedRes\x12}\n\x17StartSomniDeviceSession\x12\x30.bionode.comm.v1.SomniStartSomniDeviceSessionReq\x1a\x30.bionode.comm.v1.SomniStartSomniDeviceSessionRes\x12z\n\x16StopSomniDeviceSession\x12/.bionode.comm.v1.SomniStopSomniDeviceSessionReq\x1a/.bionode.comm.v1.SomniStopSomniDeviceSessionRes\x12S\n\tGetFusion\x12\".bionode.comm.v1.SomniGetFusionReq\x1a\".bionode.comm.v1.SomniGetFusionRes\x12Y\n\x0bGetBaseline\x12$.bionode.comm.v1.SomniGetBaselineReq\x1a$.bionode.comm.v1.SomniGetBaselineRes\x12S\n\tGetReport\x12\".bionode.comm.v1.SomniGetReportReq\x1a\".bionode.comm.v1.SomniGetReportRes\x12\x62\n\x0eGetSleepPlanet\x12\'.bionode.comm.v1.SomniGetSleepPlanetReq\x1a\'.bionode.comm.v1.SomniGetSleepPlanetRes\x12h\n\x10ListSleepPlanets\x12).bionode.comm.v1.SomniListSleepPlanetsReq\x1a).bionode.comm.v1.SomniListSleepPlanetsRes\x12z\n\x16ValidateMonitorSession\x12/.bionode.comm.v1.SomniValidateMonitorSessionReq\x1a/.bionode.comm.v1.SomniValidateMonitorSessionRes\x12k\n\x11GetMonitorPayload\x12*.bionode.comm.v1.SomniGetMonitorPayloadReq\x1a*.bionode.comm.v1.SomniGetMonitorPayloadRes\x12h\n\x10GetUidByMhrCodes\x12).bionode.comm.v1.SomniGetUidByMhrCodesReq\x1a).bionode.comm.v1.SomniGetUidByMhrCodesRes\x12k\n\x11\x43reateQuizSession\x12*.bionode.comm.v1.SomniCreateQuizSessionReq\x1a*.bionode.comm.v1.SomniCreateQuizSessionRes\x12z\n\x16UpsertSomniAppInstUser\x12/.bionode.comm.v1.SomniUpsertSomniAppInstUserReq\x1a/.bionode.comm.v1.SomniUpsertSomniAppInstUserRes\x12z\n\x16\x44\x65leteSomniAppInstUser\x12/.bionode.comm.v1.SomniDeleteSomniAppInstUserReq\x1a/.bionode.comm.v1.SomniUpsertSomniAppInstUserRes\x12U\n\rSomniQuizChat\x12!.bionode.comm.v1.SomniQuizChatReq\x1a!.bionode.comm.v1.SomniQuizChatRes\x12r\n\x17ListInterventionConfigs\x12+.bionode.comm.v1.ListInterventionConfigsReq\x1a*.bionode.comm.v1.InterventionConfigListRes\x12W\n\x15GetInterventionConfig\x12\x16.bionode.comm.v1.IdReq\x1a&.bionode.comm.v1.InterventionConfigRes\x12p\n\x18UpsertInterventionConfig\x12,.bionode.comm.v1.UpsertInterventionConfigReq\x1a&.bionode.comm.v1.InterventionConfigRes\x12M\n\x18\x44\x65leteInterventionConfig\x12\x16.bionode.comm.v1.IdReq\x1a\x19.bionode.comm.v1.EmptyRes\x12_\n\rGetPlanPhases\x12&.bionode.comm.v1.SomniGetPlanPhasesReq\x1a&.bionode.comm.v1.SomniGetPlanPhasesRes\x12w\n\x15GetInterventionEvents\x12..bionode.comm.v1.SomniGetInterventionEventsReq\x1a..bionode.comm.v1.SomniGetInterventionEventsRes\x12t\n\x14GetMonitorReportData\x12-.bionode.comm.v1.SomniGetMonitorReportDataReq\x1a-.bionode.comm.v1.SomniGetMonitorReportDataRes\x12\x83\x01\n\x19GenerateMonitorDphReports\x12\x32.bionode.comm.v1.SomniGenerateMonitorDphReportsReq\x1a\x32.bionode.comm.v1.SomniGenerateMonitorDphReportsRes\x12k\n\x11GetSessionContext\x12*.bionode.comm.v1.SomniGetSessionContextReq\x1a*.bionode.comm.v1.SomniGetSessionContextRes\x12\x80\x01\n\x18\x43onsoleStopDeviceSession\x12\x31.bionode.comm.v1.SomniConsoleStopDeviceSessionReq\x1a\x31.bionode.comm.v1.SomniConsoleStopDeviceSessionRes\x12Y\n\x13TriggerIntervention\x12\'.bionode.comm.v1.TriggerInterventionReq\x1a\x19.bionode.comm.v1.EmptyRes\x12S\n\x10StopIntervention\x12$.bionode.comm.v1.StopInterventionReq\x1a\x19.bionode.comm.v1.EmptyRes\x12k\n\x1cResetAiThoughtRehearsalState\x12\x30.bionode.comm.v1.ResetAiThoughtRehearsalStateReq\x1a\x19.bionode.comm.v1.EmptyRes\x12\x61\n\x17\x45mitDeviceLinkAiThought\x12+.bionode.comm.v1.EmitDeviceLinkAiThoughtReq\x1a\x19.bionode.comm.v1.EmptyRes\x12[\n\x0fGetChatMessages\x12#.bionode.comm.v1.GetChatMessagesReq\x1a#.bionode.comm.v1.ChatMessageListRes\x12l\n\x15ListChatConversations\x12).bionode.comm.v1.ListChatConversationsReq\x1a(.bionode.comm.v1.ChatConversationListRes\x12t\n\x19ListUserChatConversations\x12-.bionode.comm.v1.ListUserChatConversationsReq\x1a(.bionode.comm.v1.ChatConversationListRes\x12Y\n\x0bGetTempPlan\x12$.bionode.comm.v1.SomniGetTempPlanReq\x1a$.bionode.comm.v1.SomniGetTempPlanRes\x12[\n\x0fUploadChatAudio\x12#.bionode.comm.v1.UploadChatAudioReq\x1a#.bionode.comm.v1.UploadChatAudioRes\x12U\n\x11\x41ppendChatMessage\x12%.bionode.comm.v1.AppendChatMessageReq\x1a\x19.bionode.comm.v1.EmptyRes\x12}\n\x17GetSleepMapCityOverview\x12\x30.bionode.comm.v1.SomniGetSleepMapCityOverviewReq\x1a\x30.bionode.comm.v1.SomniGetSleepMapCityOverviewRes\x12w\n\x15GetSleepMapHighlights\x12..bionode.comm.v1.SomniGetSleepMapHighlightsReq\x1a..bionode.comm.v1.SomniGetSleepMapHighlightsRes\x12z\n\x16GetSleepMapCityRanking\x12/.bionode.comm.v1.SomniGetSleepMapCityRankingReq\x1a/.bionode.comm.v1.SomniGetSleepMapCityRankingRes\x12s\n\x17\x43reateDphConsoleSession\x12+.bionode.comm.v1.CreateDphConsoleSessionReq\x1a+.bionode.comm.v1.CreateDphConsoleSessionRes2\x87\t\n\x14\x41udioMaterialService\x12}\n\x1bListAudioMaterialCategories\x12/.bionode.comm.v1.ListAudioMaterialCategoriesReq\x1a-.bionode.comm.v1.AudioMaterialCategoryListRes\x12]\n\x18GetAudioMaterialCategory\x12\x16.bionode.comm.v1.IdReq\x1a).bionode.comm.v1.AudioMaterialCategoryRes\x12i\n\x1b\x43reateAudioMaterialCategory\x12/.bionode.comm.v1.CreateAudioMaterialCategoryReq\x1a\x19.bionode.comm.v1.EmptyRes\x12i\n\x1bUpdateAudioMaterialCategory\x12/.bionode.comm.v1.UpdateAudioMaterialCategoryReq\x1a\x19.bionode.comm.v1.EmptyRes\x12P\n\x1b\x44\x65leteAudioMaterialCategory\x12\x16.bionode.comm.v1.IdReq\x1a\x19.bionode.comm.v1.EmptyRes\x12\x63\n\x12ListAudioMaterials\x12&.bionode.comm.v1.ListAudioMaterialsReq\x1a%.bionode.comm.v1.AudioMaterialListRes\x12M\n\x10GetAudioMaterial\x12\x16.bionode.comm.v1.IdReq\x1a!.bionode.comm.v1.AudioMaterialRes\x12N\n\x0fGetDistinctTags\x12\x19.bionode.comm.v1.EmptyReq\x1a .bionode.comm.v1.DistinctTagsRes\x12Y\n\x13\x43reateAudioMaterial\x12\'.bionode.comm.v1.CreateAudioMaterialReq\x1a\x19.bionode.comm.v1.EmptyRes\x12Y\n\x13UpdateAudioMaterial\x12\'.bionode.comm.v1.UpdateAudioMaterialReq\x1a\x19.bionode.comm.v1.EmptyRes\x12\x65\n\x19UpdateAudioMaterialStatus\x12-.bionode.comm.v1.UpdateAudioMaterialStatusReq\x1a\x19.bionode.comm.v1.EmptyRes\x12H\n\x13\x44\x65leteAudioMaterial\x12\x16.bionode.comm.v1.IdReq\x1a\x19.bionode.comm.v1.EmptyResb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'bionode_comm_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_QUESTIONCONFIG_LABELSENTRY']._loaded_options = None - _globals['_QUESTIONCONFIG_LABELSENTRY']._serialized_options = b'8\001' - _globals['_MHRCODESRES']._serialized_start=61 - _globals['_MHRCODESRES']._serialized_end=89 - _globals['_IDREQ']._serialized_start=91 - _globals['_IDREQ']._serialized_end=110 - _globals['_EMPTYREQ']._serialized_start=112 - _globals['_EMPTYREQ']._serialized_end=122 - _globals['_EMPTYRES']._serialized_start=124 - _globals['_EMPTYRES']._serialized_end=134 - _globals['_COUNTRES']._serialized_start=136 - _globals['_COUNTRES']._serialized_end=161 - _globals['_QUESTIONOPTION']._serialized_start=163 - _globals['_QUESTIONOPTION']._serialized_end=265 - _globals['_PICKERITEM']._serialized_start=268 - _globals['_PICKERITEM']._serialized_end=505 - _globals['_QUESTIONCONFIG']._serialized_start=508 - _globals['_QUESTIONCONFIG']._serialized_end=917 - _globals['_QUESTIONCONFIG_LABELSENTRY']._serialized_start=872 - _globals['_QUESTIONCONFIG_LABELSENTRY']._serialized_end=917 - _globals['_QUESTIONINFO']._serialized_start=920 - _globals['_QUESTIONINFO']._serialized_end=1279 - _globals['_GETACTIVEQUESTIONSREQ']._serialized_start=1281 - _globals['_GETACTIVEQUESTIONSREQ']._serialized_end=1345 - _globals['_LISTQUESTIONSREQ']._serialized_start=1348 - _globals['_LISTQUESTIONSREQ']._serialized_end=1508 - _globals['_CREATEQUESTIONREQ']._serialized_start=1511 - _globals['_CREATEQUESTIONREQ']._serialized_end=1783 - _globals['_UPDATEQUESTIONREQ']._serialized_start=1786 - _globals['_UPDATEQUESTIONREQ']._serialized_end=2086 - _globals['_UPDATEQUESTIONSTATUSREQ']._serialized_start=2088 - _globals['_UPDATEQUESTIONSTATUSREQ']._serialized_end=2141 - _globals['_GETQUESTIONSBYIDSREQUEST']._serialized_start=2143 - _globals['_GETQUESTIONSBYIDSREQUEST']._serialized_end=2182 - _globals['_QUESTIONRES']._serialized_start=2184 - _globals['_QUESTIONRES']._serialized_end=2246 - _globals['_QUESTIONLISTRES']._serialized_start=2248 - _globals['_QUESTIONLISTRES']._serialized_end=2362 - _globals['_ITEMDISPLAYRULES']._serialized_start=2364 - _globals['_ITEMDISPLAYRULES']._serialized_end=2433 - _globals['_SURVEYPAGEITEM']._serialized_start=2436 - _globals['_SURVEYPAGEITEM']._serialized_end=2587 - _globals['_SURVEYPAGE']._serialized_start=2589 - _globals['_SURVEYPAGE']._serialized_end=2715 - _globals['_SURVEYSETTINGS']._serialized_start=2717 - _globals['_SURVEYSETTINGS']._serialized_end=2833 - _globals['_CODERULEOPTION']._serialized_start=2835 - _globals['_CODERULEOPTION']._serialized_end=2906 - _globals['_CODERULEQUESTION']._serialized_start=2908 - _globals['_CODERULEQUESTION']._serialized_end=2997 - _globals['_THRESHOLDITEM']._serialized_start=2999 - _globals['_THRESHOLDITEM']._serialized_end=3069 - _globals['_CODERULE']._serialized_start=3072 - _globals['_CODERULE']._serialized_end=3310 - _globals['_RESULTGUIDEITEM']._serialized_start=3312 - _globals['_RESULTGUIDEITEM']._serialized_end=3370 - _globals['_RESULTGUIDE']._serialized_start=3372 - _globals['_RESULTGUIDE']._serialized_end=3466 - _globals['_SURVEYINFO']._serialized_start=3469 - _globals['_SURVEYINFO']._serialized_end=3888 - _globals['_GETSURVEYREQ']._serialized_start=3890 - _globals['_GETSURVEYREQ']._serialized_end=3916 - _globals['_GETSURVEYBYCODEREQ']._serialized_start=3918 - _globals['_GETSURVEYBYCODEREQ']._serialized_end=3970 - _globals['_LISTSURVEYSREQ']._serialized_start=3972 - _globals['_LISTSURVEYSREQ']._serialized_end=4091 - _globals['_CREATESURVEYREQ']._serialized_start=4094 - _globals['_CREATESURVEYREQ']._serialized_end=4418 - _globals['_UPDATESURVEYREQ']._serialized_start=4421 - _globals['_UPDATESURVEYREQ']._serialized_end=4773 - _globals['_UPDATESURVEYSTATUSREQ']._serialized_start=4775 - _globals['_UPDATESURVEYSTATUSREQ']._serialized_end=4826 - _globals['_SURVEYRES']._serialized_start=4828 - _globals['_SURVEYRES']._serialized_end=4884 - _globals['_SURVEYLISTRES']._serialized_start=4886 - _globals['_SURVEYLISTRES']._serialized_end=4994 - _globals['_SURVEYPAGEWITHQUESTIONS']._serialized_start=4997 - _globals['_SURVEYPAGEWITHQUESTIONS']._serialized_end=5142 - _globals['_DISPLAYRULES']._serialized_start=5144 - _globals['_DISPLAYRULES']._serialized_end=5236 - _globals['_QUESTIONWITHMETA']._serialized_start=5239 - _globals['_QUESTIONWITHMETA']._serialized_end=5416 - _globals['_SURVEYWITHQUESTIONSRES']._serialized_start=5418 - _globals['_SURVEYWITHQUESTIONSRES']._serialized_end=5544 - _globals['_ANSWERSELECTEDOPTION']._serialized_start=5546 - _globals['_ANSWERSELECTEDOPTION']._serialized_end=5659 - _globals['_ANSWERDETAIL']._serialized_start=5662 - _globals['_ANSWERDETAIL']._serialized_end=5864 - _globals['_ANSWERINFO']._serialized_start=5867 - _globals['_ANSWERINFO']._serialized_end=6133 - _globals['_SAVEANSWERREQ']._serialized_start=6135 - _globals['_SAVEANSWERREQ']._serialized_end=6232 - _globals['_GETANSWERPROGRESSREQ']._serialized_start=6234 - _globals['_GETANSWERPROGRESSREQ']._serialized_end=6290 - _globals['_GETANSWERBYIDREQ']._serialized_start=6292 - _globals['_GETANSWERBYIDREQ']._serialized_end=6329 - _globals['_ANSWERRES']._serialized_start=6331 - _globals['_ANSWERRES']._serialized_end=6387 - _globals['_LISTANSWERSREQ']._serialized_start=6390 - _globals['_LISTANSWERSREQ']._serialized_end=6546 - _globals['_ANSWERLISTRES']._serialized_start=6548 - _globals['_ANSWERLISTRES']._serialized_end=6656 - _globals['_INDICATORS']._serialized_start=6658 - _globals['_INDICATORS']._serialized_end=6751 - _globals['_IMPROVESCENE']._serialized_start=6753 - _globals['_IMPROVESCENE']._serialized_end=6832 - _globals['_IMPROVEPLAN']._serialized_start=6834 - _globals['_IMPROVEPLAN']._serialized_end=6950 - _globals['_SCOREDETAILENTRY']._serialized_start=6952 - _globals['_SCOREDETAILENTRY']._serialized_end=7004 - _globals['_QUIZRESULTINFO']._serialized_start=7007 - _globals['_QUIZRESULTINFO']._serialized_end=7715 - _globals['_COMPUTEMHRCODESREQ']._serialized_start=7717 - _globals['_COMPUTEMHRCODESREQ']._serialized_end=7824 - _globals['_COMPUTEMHRCODESRES']._serialized_start=7826 - _globals['_COMPUTEMHRCODESRES']._serialized_end=7865 - _globals['_SUBMITQUIZREQ']._serialized_start=7867 - _globals['_SUBMITQUIZREQ']._serialized_end=7953 - _globals['_SUBMITSCALEQUIZREQ']._serialized_start=7956 - _globals['_SUBMITSCALEQUIZREQ']._serialized_end=8085 - _globals['_GETLATESTRESULTREQ']._serialized_start=8087 - _globals['_GETLATESTRESULTREQ']._serialized_end=8141 - _globals['_GETRESULTBYIDREQ']._serialized_start=8143 - _globals['_GETRESULTBYIDREQ']._serialized_end=8180 - _globals['_LISTRESULTSREQ']._serialized_start=8183 - _globals['_LISTRESULTSREQ']._serialized_end=8316 - _globals['_QUIZRESULTRES']._serialized_start=8318 - _globals['_QUIZRESULTRES']._serialized_end=8382 - _globals['_QUIZRESULTLISTRES']._serialized_start=8384 - _globals['_QUIZRESULTLISTRES']._serialized_end=8500 - _globals['_INDICATORDESCRIPTIONS']._serialized_start=8502 - _globals['_INDICATORDESCRIPTIONS']._serialized_end=8606 - _globals['_SCENEITEM']._serialized_start=8608 - _globals['_SCENEITEM']._serialized_end=8684 - _globals['_SCHEMECOLORSTOP']._serialized_start=8686 - _globals['_SCHEMECOLORSTOP']._serialized_end=8790 - _globals['_LIGHTEXITEFFECTTARGET']._serialized_start=8792 - _globals['_LIGHTEXITEFFECTTARGET']._serialized_end=8885 - _globals['_LIGHTEXITEFFECT']._serialized_start=8887 - _globals['_LIGHTEXITEFFECT']._serialized_end=8995 - _globals['_SCHEMELIGHT']._serialized_start=8998 - _globals['_SCHEMELIGHT']._serialized_end=9203 - _globals['_SCHEMETRACK']._serialized_start=9205 - _globals['_SCHEMETRACK']._serialized_end=9300 - _globals['_SCHEMESOUND']._serialized_start=9302 - _globals['_SCHEMESOUND']._serialized_end=9413 - _globals['_SCHEMESCENTSLOT']._serialized_start=9416 - _globals['_SCHEMESCENTSLOT']._serialized_end=9547 - _globals['_SCHEMESCENT']._serialized_start=9549 - _globals['_SCHEMESCENT']._serialized_end=9663 - _globals['_SCHEMETEMP']._serialized_start=9665 - _globals['_SCHEMETEMP']._serialized_end=9750 - _globals['_SCHEMEITEM']._serialized_start=9753 - _globals['_SCHEMEITEM']._serialized_end=9979 - _globals['_PERIOD']._serialized_start=9982 - _globals['_PERIOD']._serialized_end=10182 - _globals['_SHARECONFIG']._serialized_start=10185 - _globals['_SHARECONFIG']._serialized_end=10314 - _globals['_PERSONALITYINFO']._serialized_start=10317 - _globals['_PERSONALITYINFO']._serialized_end=10807 - _globals['_GETPERSONALITYREQ']._serialized_start=10809 - _globals['_GETPERSONALITYREQ']._serialized_end=10865 - _globals['_LISTPERSONALITIESREQ']._serialized_start=10867 - _globals['_LISTPERSONALITIESREQ']._serialized_end=10988 - _globals['_CREATEPERSONALITYREQ']._serialized_start=10991 - _globals['_CREATEPERSONALITYREQ']._serialized_end=11416 - _globals['_UPDATEPERSONALITYREQ']._serialized_start=11419 - _globals['_UPDATEPERSONALITYREQ']._serialized_end=11872 - _globals['_UPDATEPERSONALITYPERIODSREQ']._serialized_start=11874 - _globals['_UPDATEPERSONALITYPERIODSREQ']._serialized_end=11957 - _globals['_UPDATEPERSONALITYSTATUSREQ']._serialized_start=11959 - _globals['_UPDATEPERSONALITYSTATUSREQ']._serialized_end=12015 - _globals['_PERSONALITYRES']._serialized_start=12017 - _globals['_PERSONALITYRES']._serialized_end=12088 - _globals['_PERSONALITYLISTRES']._serialized_start=12090 - _globals['_PERSONALITYLISTRES']._serialized_end=12214 - _globals['_SHAREINFO']._serialized_start=12217 - _globals['_SHAREINFO']._serialized_end=12351 - _globals['_CREATESHAREREQ']._serialized_start=12353 - _globals['_CREATESHAREREQ']._serialized_end=12441 - _globals['_SHARERES']._serialized_start=12443 - _globals['_SHARERES']._serialized_end=12496 - _globals['_GETSHARELANDINGREQ']._serialized_start=12498 - _globals['_GETSHARELANDINGREQ']._serialized_end=12536 - _globals['_SHARELANDINGRES']._serialized_start=12539 - _globals['_SHARELANDINGRES']._serialized_end=12724 - _globals['_RECORDVISITREQ']._serialized_start=12726 - _globals['_RECORDVISITREQ']._serialized_end=12781 - _globals['_SHARETRACKINGITEM']._serialized_start=12784 - _globals['_SHARETRACKINGITEM']._serialized_end=12957 - _globals['_LISTSHARETRACKINGREQ']._serialized_start=12959 - _globals['_LISTSHARETRACKINGREQ']._serialized_end=13027 - _globals['_SHARETRACKINGLISTRES']._serialized_start=13029 - _globals['_SHARETRACKINGLISTRES']._serialized_end=13153 - _globals['_SHARERRANKINGITEM']._serialized_start=13156 - _globals['_SHARERRANKINGITEM']._serialized_end=13356 - _globals['_LISTSHARERRANKINGREQ']._serialized_start=13359 - _globals['_LISTSHARERRANKINGREQ']._serialized_end=13508 - _globals['_SHARERRANKINGLISTRES']._serialized_start=13510 - _globals['_SHARERRANKINGLISTRES']._serialized_end=13633 - _globals['_SHAREITEM']._serialized_start=13636 - _globals['_SHAREITEM']._serialized_end=13833 - _globals['_LISTSHARESBYUSERREQ']._serialized_start=13835 - _globals['_LISTSHARESBYUSERREQ']._serialized_end=13915 - _globals['_SHARELISTRES']._serialized_start=13917 - _globals['_SHARELISTRES']._serialized_end=14022 - _globals['_SHAREVISITITEM']._serialized_start=14025 - _globals['_SHAREVISITITEM']._serialized_end=14184 - _globals['_LISTSHAREVISITSREQ']._serialized_start=14186 - _globals['_LISTSHAREVISITSREQ']._serialized_end=14270 - _globals['_SHAREVISITLISTRES']._serialized_start=14272 - _globals['_SHAREVISITLISTRES']._serialized_end=14387 - _globals['_CLIENTINFO']._serialized_start=14389 - _globals['_CLIENTINFO']._serialized_end=14484 - _globals['_TRACKEVENTREQ']._serialized_start=14487 - _globals['_TRACKEVENTREQ']._serialized_end=14752 - _globals['_BATCHTRACKREQ']._serialized_start=14754 - _globals['_BATCHTRACKREQ']._serialized_end=14817 - _globals['_GETOVERVIEWREQ']._serialized_start=14819 - _globals['_GETOVERVIEWREQ']._serialized_end=14889 - _globals['_OVERVIEWRES']._serialized_start=14892 - _globals['_OVERVIEWRES']._serialized_end=15027 - _globals['_GETPAGEVIEWUVREQ']._serialized_start=15029 - _globals['_GETPAGEVIEWUVREQ']._serialized_end=15101 - _globals['_GETQUIZCOMPLETEUVREQ']._serialized_start=15103 - _globals['_GETQUIZCOMPLETEUVREQ']._serialized_end=15179 - _globals['_GETINTERACTIONCOUNTREQ']._serialized_start=15181 - _globals['_GETINTERACTIONCOUNTREQ']._serialized_end=15259 - _globals['_LISTLOGEVENTSREQ']._serialized_start=15262 - _globals['_LISTLOGEVENTSREQ']._serialized_end=15467 - _globals['_LOGEVENTITEM']._serialized_start=15470 - _globals['_LOGEVENTITEM']._serialized_end=15755 - _globals['_LISTLOGEVENTSRES']._serialized_start=15757 - _globals['_LISTLOGEVENTSRES']._serialized_end=15869 - _globals['_GETEVENTTRENDREQ']._serialized_start=15871 - _globals['_GETEVENTTRENDREQ']._serialized_end=15943 - _globals['_EVENTTRENDRES']._serialized_start=15945 - _globals['_EVENTTRENDRES']._serialized_end=16009 - _globals['_GETEVENTFILTEROPTIONSREQ']._serialized_start=16011 - _globals['_GETEVENTFILTEROPTIONSREQ']._serialized_end=16053 - _globals['_EVENTFILTEROPTIONSRES']._serialized_start=16055 - _globals['_EVENTFILTEROPTIONSRES']._serialized_end=16110 - _globals['_AUDIOMATERIALCATEGORYINFO']._serialized_start=16113 - _globals['_AUDIOMATERIALCATEGORYINFO']._serialized_end=16357 - _globals['_LISTAUDIOMATERIALCATEGORIESREQ']._serialized_start=16360 - _globals['_LISTAUDIOMATERIALCATEGORIESREQ']._serialized_end=16535 - _globals['_AUDIOMATERIALCATEGORYLISTRES']._serialized_start=16538 - _globals['_AUDIOMATERIALCATEGORYLISTRES']._serialized_end=16679 - _globals['_AUDIOMATERIALCATEGORYRES']._serialized_start=16681 - _globals['_AUDIOMATERIALCATEGORYRES']._serialized_end=16769 - _globals['_CREATEAUDIOMATERIALCATEGORYREQ']._serialized_start=16772 - _globals['_CREATEAUDIOMATERIALCATEGORYREQ']._serialized_end=16942 - _globals['_UPDATEAUDIOMATERIALCATEGORYREQ']._serialized_start=16945 - _globals['_UPDATEAUDIOMATERIALCATEGORYREQ']._serialized_end=17225 - _globals['_AUDIOMATERIALTAGVALUE']._serialized_start=17227 - _globals['_AUDIOMATERIALTAGVALUE']._serialized_end=17294 - _globals['_AUDIOMATERIALTAG']._serialized_start=17297 - _globals['_AUDIOMATERIALTAG']._serialized_end=17634 - _globals['_AUDIOMATERIALINFO']._serialized_start=17637 - _globals['_AUDIOMATERIALINFO']._serialized_end=18225 - _globals['_LISTAUDIOMATERIALSREQ']._serialized_start=18228 - _globals['_LISTAUDIOMATERIALSREQ']._serialized_end=18357 - _globals['_AUDIOMATERIALLISTRES']._serialized_start=18359 - _globals['_AUDIOMATERIALLISTRES']._serialized_end=18483 - _globals['_AUDIOMATERIALRES']._serialized_start=18485 - _globals['_AUDIOMATERIALRES']._serialized_end=18557 - _globals['_DISTINCTTAGSRES']._serialized_start=18559 - _globals['_DISTINCTTAGSRES']._serialized_end=18590 - _globals['_CREATEAUDIOMATERIALREQ']._serialized_start=18593 - _globals['_CREATEAUDIOMATERIALREQ']._serialized_end=19135 - _globals['_UPDATEAUDIOMATERIALREQ']._serialized_start=19138 - _globals['_UPDATEAUDIOMATERIALREQ']._serialized_end=19848 - _globals['_UPDATEAUDIOMATERIALSTATUSREQ']._serialized_start=19850 - _globals['_UPDATEAUDIOMATERIALSTATUSREQ']._serialized_end=19908 - _globals['_SOMNISETALARMREQ']._serialized_start=19910 - _globals['_SOMNISETALARMREQ']._serialized_end=20032 - _globals['_SOMNISETALARMRES']._serialized_start=20034 - _globals['_SOMNISETALARMRES']._serialized_end=20126 - _globals['_SOMNIGETPLANREQ']._serialized_start=20128 - _globals['_SOMNIGETPLANREQ']._serialized_end=20178 - _globals['_SOMNIGETPLANRES']._serialized_start=20180 - _globals['_SOMNIGETPLANRES']._serialized_end=20303 - _globals['_SOMNIMARKPLANSTARTEDREQ']._serialized_start=20305 - _globals['_SOMNIMARKPLANSTARTEDREQ']._serialized_end=20363 - _globals['_SOMNIMARKPLANSTARTEDRES']._serialized_start=20365 - _globals['_SOMNIMARKPLANSTARTEDRES']._serialized_end=20452 - _globals['_SOMNIMARKPLANSTOPPEDREQ']._serialized_start=20454 - _globals['_SOMNIMARKPLANSTOPPEDREQ']._serialized_end=20512 - _globals['_SOMNIMARKPLANSTOPPEDRES']._serialized_start=20514 - _globals['_SOMNIMARKPLANSTOPPEDRES']._serialized_end=20564 - _globals['_SOMNIPHASEDURATIONPATCH']._serialized_start=20566 - _globals['_SOMNIPHASEDURATIONPATCH']._serialized_end=20628 - _globals['_SOMNISTARTSOMNIDEVICESESSIONREQ']._serialized_start=20631 - _globals['_SOMNISTARTSOMNIDEVICESESSIONREQ']._serialized_end=20814 - _globals['_SOMNISTARTSOMNIDEVICESESSIONRES']._serialized_start=20816 - _globals['_SOMNISTARTSOMNIDEVICESESSIONRES']._serialized_end=20893 - _globals['_SOMNISTOPSOMNIDEVICESESSIONREQ']._serialized_start=20895 - _globals['_SOMNISTOPSOMNIDEVICESESSIONREQ']._serialized_end=20960 - _globals['_SOMNISTOPSOMNIDEVICESESSIONRES']._serialized_start=20962 - _globals['_SOMNISTOPSOMNIDEVICESESSIONRES']._serialized_end=21019 - _globals['_SOMNIBASELINEFUSIONKEYVALINT32']._serialized_start=21021 - _globals['_SOMNIBASELINEFUSIONKEYVALINT32']._serialized_end=21079 - _globals['_SOMNIBASELINEFUSIONKEYVALBOOL']._serialized_start=21081 - _globals['_SOMNIBASELINEFUSIONKEYVALBOOL']._serialized_end=21138 - _globals['_SOMNIBASELINEFUSIONSTEPS']._serialized_start=21140 - _globals['_SOMNIBASELINEFUSIONSTEPS']._serialized_end=21250 - _globals['_SOMNIBASELINEFUSIONCALENDAR']._serialized_start=21252 - _globals['_SOMNIBASELINEFUSIONCALENDAR']._serialized_end=21343 - _globals['_SOMNIBASELINEFUSIONWEATHER']._serialized_start=21346 - _globals['_SOMNIBASELINEFUSIONWEATHER']._serialized_end=21655 - _globals['_SOMNIBASELINEFUSIONSCORE']._serialized_start=21657 - _globals['_SOMNIBASELINEFUSIONSCORE']._serialized_end=21761 - _globals['_SOMNIBASELINEFUSIONDAILY']._serialized_start=21764 - _globals['_SOMNIBASELINEFUSIONDAILY']._serialized_end=22032 - _globals['_SOMNIGETFUSIONREQ']._serialized_start=22034 - _globals['_SOMNIGETFUSIONREQ']._serialized_end=22098 - _globals['_SOMNIGETFUSIONRES']._serialized_start=22100 - _globals['_SOMNIGETFUSIONRES']._serialized_end=22158 - _globals['_SOMNIGETBASELINEREQ']._serialized_start=22160 - _globals['_SOMNIGETBASELINEREQ']._serialized_end=22264 - _globals['_SOMNIGETBASELINERES']._serialized_start=22267 - _globals['_SOMNIGETBASELINERES']._serialized_end=22485 - _globals['_SOMNIGETREPORTREQ']._serialized_start=22487 - _globals['_SOMNIGETREPORTREQ']._serialized_end=22558 - _globals['_SOMNIGETREPORTRES']._serialized_start=22560 - _globals['_SOMNIGETREPORTRES']._serialized_end=22616 - _globals['_SOMNISLEEPPLANETILLUSTRATIONITEM']._serialized_start=22618 - _globals['_SOMNISLEEPPLANETILLUSTRATIONITEM']._serialized_end=22685 - _globals['_SOMNISLEEPPLANETPARAMS']._serialized_start=22688 - _globals['_SOMNISLEEPPLANETPARAMS']._serialized_end=22845 - _globals['_SOMNISLEEPPLANETSTABILITY']._serialized_start=22847 - _globals['_SOMNISLEEPPLANETSTABILITY']._serialized_end=22910 - _globals['_SOMNISLEEPPLANETMETRICS']._serialized_start=22913 - _globals['_SOMNISLEEPPLANETMETRICS']._serialized_end=23101 - _globals['_SOMNISLEEPPLANETDETAIL']._serialized_start=23104 - _globals['_SOMNISLEEPPLANETDETAIL']._serialized_end=23388 - _globals['_SOMNIGETSLEEPPLANETREQ']._serialized_start=23390 - _globals['_SOMNIGETSLEEPPLANETREQ']._serialized_end=23448 - _globals['_SOMNIGETSLEEPPLANETRES']._serialized_start=23450 - _globals['_SOMNIGETSLEEPPLANETRES']._serialized_end=23546 - _globals['_SOMNILISTSLEEPPLANETSREQ']._serialized_start=23548 - _globals['_SOMNILISTSLEEPPLANETSREQ']._serialized_end=23633 - _globals['_SOMNILISTSLEEPPLANETSRES']._serialized_start=23636 - _globals['_SOMNILISTSLEEPPLANETSRES']._serialized_end=23767 - _globals['_SOMNIVALIDATEMONITORSESSIONREQ']._serialized_start=23769 - _globals['_SOMNIVALIDATEMONITORSESSIONREQ']._serialized_end=23834 - _globals['_SOMNIVALIDATEMONITORSESSIONRES']._serialized_start=23836 - _globals['_SOMNIVALIDATEMONITORSESSIONRES']._serialized_end=23880 - _globals['_SOMNIGETMONITORPAYLOADREQ']._serialized_start=23882 - _globals['_SOMNIGETMONITORPAYLOADREQ']._serialized_end=23942 - _globals['_SOMNIGETMONITORPAYLOADRES']._serialized_start=23944 - _globals['_SOMNIGETMONITORPAYLOADRES']._serialized_end=23988 - _globals['_SOMNIGETUIDBYMHRCODESREQ']._serialized_start=23990 - _globals['_SOMNIGETUIDBYMHRCODESREQ']._serialized_end=24056 - _globals['_SOMNIGETUIDBYMHRCODESRES']._serialized_start=24058 - _globals['_SOMNIGETUIDBYMHRCODESRES']._serialized_end=24097 - _globals['_SOMNICREATEQUIZSESSIONREQ']._serialized_start=24099 - _globals['_SOMNICREATEQUIZSESSIONREQ']._serialized_end=24205 - _globals['_SOMNICREATEQUIZSESSIONRES']._serialized_start=24207 - _globals['_SOMNICREATEQUIZSESSIONRES']._serialized_end=24254 - _globals['_SOMNIUPSERTSOMNIAPPINSTUSERREQ']._serialized_start=24256 - _globals['_SOMNIUPSERTSOMNIAPPINSTUSERREQ']._serialized_end=24326 - _globals['_SOMNIUPSERTSOMNIAPPINSTUSERRES']._serialized_start=24328 - _globals['_SOMNIUPSERTSOMNIAPPINSTUSERRES']._serialized_end=24385 - _globals['_SOMNIDELETESOMNIAPPINSTUSERREQ']._serialized_start=24387 - _globals['_SOMNIDELETESOMNIAPPINSTUSERREQ']._serialized_end=24444 - _globals['_SOMNIQUIZPENDINGOPTION']._serialized_start=24446 - _globals['_SOMNIQUIZPENDINGOPTION']._serialized_end=24510 - _globals['_SOMNIQUIZCONFIGITEMCONFIG']._serialized_start=24512 - _globals['_SOMNIQUIZCONFIGITEMCONFIG']._serialized_end=24579 - _globals['_SOMNIQUIZPENDINGQUESTIONCONFIGITEM']._serialized_start=24582 - _globals['_SOMNIQUIZPENDINGQUESTIONCONFIGITEM']._serialized_end=24838 - _globals['_SOMNIQUIZPENDINGQUESTIONCONFIG']._serialized_start=24840 - _globals['_SOMNIQUIZPENDINGQUESTIONCONFIG']._serialized_end=24940 - _globals['_SOMNIQUIZPENDINGQUESTION']._serialized_start=24943 - _globals['_SOMNIQUIZPENDINGQUESTION']._serialized_end=25175 - _globals['_SOMNIQUIZDIRECTANSWERIN']._serialized_start=25177 - _globals['_SOMNIQUIZDIRECTANSWERIN']._serialized_end=25270 - _globals['_SOMNIQUIZREPORTSECTION']._serialized_start=25272 - _globals['_SOMNIQUIZREPORTSECTION']._serialized_end=25328 - _globals['_SOMNIQUIZCHATREQ']._serialized_start=25331 - _globals['_SOMNIQUIZCHATREQ']._serialized_end=25613 - _globals['_SOMNIQUIZCHATRES']._serialized_start=25616 - _globals['_SOMNIQUIZCHATRES']._serialized_end=25968 - _globals['_INTERVENTIONCONDITION']._serialized_start=25970 - _globals['_INTERVENTIONCONDITION']._serialized_end=26091 - _globals['_INTERVENTIONSCENARIO']._serialized_start=26094 - _globals['_INTERVENTIONSCENARIO']._serialized_end=26222 - _globals['_INTERVENTIONCONFIGINFO']._serialized_start=26225 - _globals['_INTERVENTIONCONFIGINFO']._serialized_end=26584 - _globals['_LISTINTERVENTIONCONFIGSREQ']._serialized_start=26586 - _globals['_LISTINTERVENTIONCONFIGSREQ']._serialized_end=26614 - _globals['_INTERVENTIONCONFIGLISTRES']._serialized_start=26616 - _globals['_INTERVENTIONCONFIGLISTRES']._serialized_end=26698 - _globals['_UPSERTINTERVENTIONCONFIGREQ']._serialized_start=26701 - _globals['_UPSERTINTERVENTIONCONFIGREQ']._serialized_end=27023 - _globals['_INTERVENTIONCONFIGRES']._serialized_start=27025 - _globals['_INTERVENTIONCONFIGRES']._serialized_end=27105 - _globals['_TRIGGERINTERVENTIONREQ']._serialized_start=27107 - _globals['_TRIGGERINTERVENTIONREQ']._serialized_end=27165 - _globals['_STOPINTERVENTIONREQ']._serialized_start=27167 - _globals['_STOPINTERVENTIONREQ']._serialized_end=27208 - _globals['_RESETAITHOUGHTREHEARSALSTATEREQ']._serialized_start=27210 - _globals['_RESETAITHOUGHTREHEARSALSTATEREQ']._serialized_end=27263 - _globals['_EMITDEVICELINKAITHOUGHTREQ']._serialized_start=27265 - _globals['_EMITDEVICELINKAITHOUGHTREQ']._serialized_end=27344 - _globals['_SOMNIGETPLANPHASESREQ']._serialized_start=27346 - _globals['_SOMNIGETPLANPHASESREQ']._serialized_end=27389 - _globals['_SOMNIPLANPHASEITEM']._serialized_start=27392 - _globals['_SOMNIPLANPHASEITEM']._serialized_end=27580 - _globals['_SOMNIGETPLANPHASESRES']._serialized_start=27582 - _globals['_SOMNIGETPLANPHASESRES']._serialized_end=27658 - _globals['_SOMNIGETINTERVENTIONEVENTSREQ']._serialized_start=27660 - _globals['_SOMNIGETINTERVENTIONEVENTSREQ']._serialized_end=27711 - _globals['_SOMNIINTERVENTIONEVENTITEM']._serialized_start=27714 - _globals['_SOMNIINTERVENTIONEVENTITEM']._serialized_end=27917 - _globals['_SOMNIGETINTERVENTIONEVENTSRES']._serialized_start=27919 - _globals['_SOMNIGETINTERVENTIONEVENTSRES']._serialized_end=28011 - _globals['_SOMNIGETMONITORREPORTDATAREQ']._serialized_start=28013 - _globals['_SOMNIGETMONITORREPORTDATAREQ']._serialized_end=28084 - _globals['_SOMNIGENERATEMONITORDPHREPORTSREQ']._serialized_start=28086 - _globals['_SOMNIGENERATEMONITORDPHREPORTSREQ']._serialized_end=28141 - _globals['_SOMNIGENERATEMONITORDPHREPORTSRES']._serialized_start=28143 - _globals['_SOMNIGENERATEMONITORDPHREPORTSRES']._serialized_end=28210 - _globals['_PHYSIOLOGICALDATAPOINT']._serialized_start=28212 - _globals['_PHYSIOLOGICALDATAPOINT']._serialized_end=28317 - _globals['_ENVIRONMENTDATAPOINT']._serialized_start=28319 - _globals['_ENVIRONMENTDATAPOINT']._serialized_end=28430 - _globals['_INTERVENTIONSUMMARY']._serialized_start=28432 - _globals['_INTERVENTIONSUMMARY']._serialized_end=28535 - _globals['_PERSONALITYHIGHLIGHT']._serialized_start=28537 - _globals['_PERSONALITYHIGHLIGHT']._serialized_end=28621 - _globals['_RADARMETRICS']._serialized_start=28624 - _globals['_RADARMETRICS']._serialized_end=28777 - _globals['_RADARRESULT']._serialized_start=28780 - _globals['_RADARRESULT']._serialized_end=28915 - _globals['_ANOMALYSEGMENT']._serialized_start=28917 - _globals['_ANOMALYSEGMENT']._serialized_end=29034 - _globals['_ANOMALYSTATUS']._serialized_start=29037 - _globals['_ANOMALYSTATUS']._serialized_end=29186 - _globals['_ENVIRONMENTCARDSERIES']._serialized_start=29188 - _globals['_ENVIRONMENTCARDSERIES']._serialized_end=29240 - _globals['_CLIMATECARD']._serialized_start=29243 - _globals['_CLIMATECARD']._serialized_end=29375 - _globals['_ENVIRONMENTCARDS']._serialized_start=29378 - _globals['_ENVIRONMENTCARDS']._serialized_end=29637 - _globals['_PLANOVERVIEWRGB']._serialized_start=29639 - _globals['_PLANOVERVIEWRGB']._serialized_end=29689 - _globals['_PLANOVERVIEWSCENT']._serialized_start=29691 - _globals['_PLANOVERVIEWSCENT']._serialized_end=29744 - _globals['_PLANOVERVIEWLIGHTPHASE']._serialized_start=29746 - _globals['_PLANOVERVIEWLIGHTPHASE']._serialized_end=29840 - _globals['_PLANOVERVIEWS']._serialized_start=29843 - _globals['_PLANOVERVIEWS']._serialized_end=30012 - _globals['_SOMNIQUIZUSERPROFILESLEEPWAKETIME']._serialized_start=30014 - _globals['_SOMNIQUIZUSERPROFILESLEEPWAKETIME']._serialized_end=30088 - _globals['_SOMNIQUIZUSERPROFILE']._serialized_start=30091 - _globals['_SOMNIQUIZUSERPROFILE']._serialized_end=30259 - _globals['_SOMNIGETMONITORREPORTDATARES']._serialized_start=30262 - _globals['_SOMNIGETMONITORREPORTDATARES']._serialized_end=30785 - _globals['_SOMNIGETSESSIONCONTEXTREQ']._serialized_start=30787 - _globals['_SOMNIGETSESSIONCONTEXTREQ']._serialized_end=30834 - _globals['_SOMNISESSIONCONTEXTPLANPHASE']._serialized_start=30837 - _globals['_SOMNISESSIONCONTEXTPLANPHASE']._serialized_end=31017 - _globals['_SOMNIGETSESSIONCONTEXTRES']._serialized_start=31020 - _globals['_SOMNIGETSESSIONCONTEXTRES']._serialized_end=31155 - _globals['_SOMNICONSOLESTOPDEVICESESSIONREQ']._serialized_start=31157 - _globals['_SOMNICONSOLESTOPDEVICESESSIONREQ']._serialized_end=31211 - _globals['_SOMNICONSOLESTOPDEVICESESSIONRES']._serialized_start=31213 - _globals['_SOMNICONSOLESTOPDEVICESESSIONRES']._serialized_end=31272 - _globals['_CHATMESSAGEINFO']._serialized_start=31275 - _globals['_CHATMESSAGEINFO']._serialized_end=31451 - _globals['_GETCHATMESSAGESREQ']._serialized_start=31453 - _globals['_GETCHATMESSAGESREQ']._serialized_end=31498 - _globals['_CHATMESSAGELISTRES']._serialized_start=31500 - _globals['_CHATMESSAGELISTRES']._serialized_end=31568 - _globals['_CHATCONVERSATIONITEM']._serialized_start=31571 - _globals['_CHATCONVERSATIONITEM']._serialized_end=31714 - _globals['_LISTCHATCONVERSATIONSREQ']._serialized_start=31717 - _globals['_LISTCHATCONVERSATIONSREQ']._serialized_end=31845 - _globals['_LISTUSERCHATCONVERSATIONSREQ']._serialized_start=31847 - _globals['_LISTUSERCHATCONVERSATIONSREQ']._serialized_end=31954 - _globals['_CHATCONVERSATIONLISTRES']._serialized_start=31957 - _globals['_CHATCONVERSATIONLISTRES']._serialized_end=32091 - _globals['_SOMNIGETTEMPPLANREQ']._serialized_start=32093 - _globals['_SOMNIGETTEMPPLANREQ']._serialized_end=32165 - _globals['_SOMNIGETTEMPPLANRES']._serialized_start=32167 - _globals['_SOMNIGETTEMPPLANRES']._serialized_end=32252 - _globals['_UPLOADCHATAUDIOREQ']._serialized_start=32254 - _globals['_UPLOADCHATAUDIOREQ']._serialized_end=32310 - _globals['_UPLOADCHATAUDIORES']._serialized_start=32312 - _globals['_UPLOADCHATAUDIORES']._serialized_end=32351 - _globals['_APPENDCHATMESSAGEREQ']._serialized_start=32353 - _globals['_APPENDCHATMESSAGEREQ']._serialized_end=32478 - _globals['_SOMNISLEEPMAPREGION']._serialized_start=32481 - _globals['_SOMNISLEEPMAPREGION']._serialized_end=32636 - _globals['_SOMNISLEEPMAPDIMENSIONGRPC']._serialized_start=32638 - _globals['_SOMNISLEEPMAPDIMENSIONGRPC']._serialized_end=32760 - _globals['_SOMNISLEEPMAPDIMENSIONSGRPC']._serialized_start=32763 - _globals['_SOMNISLEEPMAPDIMENSIONSGRPC']._serialized_end=33140 - _globals['_SOMNISLEEPMAPANALYSISGRPC']._serialized_start=33143 - _globals['_SOMNISLEEPMAPANALYSISGRPC']._serialized_end=33493 - _globals['_SOMNISLEEPMAPDISTRICTGRPC']._serialized_start=33496 - _globals['_SOMNISLEEPMAPDISTRICTGRPC']._serialized_end=33692 - _globals['_SOMNISLEEPMAPCITYSUMMARYGRPC']._serialized_start=33695 - _globals['_SOMNISLEEPMAPCITYSUMMARYGRPC']._serialized_end=33869 - _globals['_SOMNIGETSLEEPMAPCITYOVERVIEWREQ']._serialized_start=33872 - _globals['_SOMNIGETSLEEPMAPCITYOVERVIEWREQ']._serialized_end=34021 - _globals['_SOMNIGETSLEEPMAPCITYOVERVIEWRES']._serialized_start=34024 - _globals['_SOMNIGETSLEEPMAPCITYOVERVIEWRES']._serialized_end=34244 - _globals['_SOMNIGETSLEEPMAPHIGHLIGHTSREQ']._serialized_start=34246 - _globals['_SOMNIGETSLEEPMAPHIGHLIGHTSREQ']._serialized_end=34341 - _globals['_SOMNIGETSLEEPMAPHIGHLIGHTSRES']._serialized_start=34344 - _globals['_SOMNIGETSLEEPMAPHIGHLIGHTSRES']._serialized_end=34508 - _globals['_SOMNISLEEPMAPLEADERBOARDROWGRPC']._serialized_start=34511 - _globals['_SOMNISLEEPMAPLEADERBOARDROWGRPC']._serialized_end=34686 - _globals['_SOMNISLEEPMAPCURRENTUSERSUMMARYGRPC']._serialized_start=34689 - _globals['_SOMNISLEEPMAPCURRENTUSERSUMMARYGRPC']._serialized_end=34844 - _globals['_SOMNIGETSLEEPMAPCITYRANKINGREQ']._serialized_start=34846 - _globals['_SOMNIGETSLEEPMAPCITYRANKINGREQ']._serialized_end=34944 - _globals['_SOMNIGETSLEEPMAPCITYRANKINGRES']._serialized_start=34947 - _globals['_SOMNIGETSLEEPMAPCITYRANKINGRES']._serialized_end=35215 - _globals['_CREATEDPHCONSOLESESSIONREQ']._serialized_start=35217 - _globals['_CREATEDPHCONSOLESESSIONREQ']._serialized_end=35319 - _globals['_CREATEDPHCONSOLESESSIONRES']._serialized_start=35321 - _globals['_CREATEDPHCONSOLESESSIONRES']._serialized_end=35369 - _globals['_QUIZSERVICE']._serialized_start=35372 - _globals['_QUIZSERVICE']._serialized_end=36865 - _globals['_SURVEYSERVICE']._serialized_start=36868 - _globals['_SURVEYSERVICE']._serialized_end=38532 - _globals['_SHARESERVICE']._serialized_start=38535 - _globals['_SHARESERVICE']._serialized_end=39168 - _globals['_EVENTSERVICE']._serialized_start=39171 - _globals['_EVENTSERVICE']._serialized_end=39945 - _globals['_SOMNISERVICE']._serialized_start=39948 - _globals['_SOMNISERVICE']._serialized_end=44403 - _globals['_AUDIOMATERIALSERVICE']._serialized_start=44406 - _globals['_AUDIOMATERIALSERVICE']._serialized_end=45565 -# @@protoc_insertion_point(module_scope) diff --git a/app/bionode_grpc_clients/comm/grpc_gen/bionode_comm_pb2_grpc.py b/app/bionode_grpc_clients/comm/grpc_gen/bionode_comm_pb2_grpc.py deleted file mode 100644 index e54900c..0000000 --- a/app/bionode_grpc_clients/comm/grpc_gen/bionode_comm_pb2_grpc.py +++ /dev/null @@ -1,4932 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - -from . import bionode_comm_pb2 as bionode__comm__pb2 - -GRPC_GENERATED_VERSION = '1.81.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in bionode_comm_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) - - -class QuizServiceStub: - """============================================================ - Comm Service — 通用业务微服务 - 端口: gRPC :50062 - 包含: QuizService / SurveyService / ShareService / EventService - ============================================================ - - ==================== QuizService ==================== - 题库管理(全局题目池) - 对应表: quiz_questions - - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.GetActiveQuestions = channel.unary_unary( - '/bionode.comm.v1.QuizService/GetActiveQuestions', - request_serializer=bionode__comm__pb2.GetActiveQuestionsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.QuestionListRes.FromString, - _registered_method=True) - self.ListQuestions = channel.unary_unary( - '/bionode.comm.v1.QuizService/ListQuestions', - request_serializer=bionode__comm__pb2.ListQuestionsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.QuestionListRes.FromString, - _registered_method=True) - self.GetQuestion = channel.unary_unary( - '/bionode.comm.v1.QuizService/GetQuestion', - request_serializer=bionode__comm__pb2.IdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.QuestionRes.FromString, - _registered_method=True) - self.CreateQuestion = channel.unary_unary( - '/bionode.comm.v1.QuizService/CreateQuestion', - request_serializer=bionode__comm__pb2.CreateQuestionReq.SerializeToString, - response_deserializer=bionode__comm__pb2.QuestionRes.FromString, - _registered_method=True) - self.UpdateQuestion = channel.unary_unary( - '/bionode.comm.v1.QuizService/UpdateQuestion', - request_serializer=bionode__comm__pb2.UpdateQuestionReq.SerializeToString, - response_deserializer=bionode__comm__pb2.QuestionRes.FromString, - _registered_method=True) - self.UpdateQuestionStatus = channel.unary_unary( - '/bionode.comm.v1.QuizService/UpdateQuestionStatus', - request_serializer=bionode__comm__pb2.UpdateQuestionStatusReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.DeleteQuestion = channel.unary_unary( - '/bionode.comm.v1.QuizService/DeleteQuestion', - request_serializer=bionode__comm__pb2.IdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.GetQuestionsByIds = channel.unary_unary( - '/bionode.comm.v1.QuizService/GetQuestionsByIds', - request_serializer=bionode__comm__pb2.GetQuestionsByIdsRequest.SerializeToString, - response_deserializer=bionode__comm__pb2.QuestionListRes.FromString, - _registered_method=True) - self.GetPersonality = channel.unary_unary( - '/bionode.comm.v1.QuizService/GetPersonality', - request_serializer=bionode__comm__pb2.GetPersonalityReq.SerializeToString, - response_deserializer=bionode__comm__pb2.PersonalityRes.FromString, - _registered_method=True) - self.GetPersonalityById = channel.unary_unary( - '/bionode.comm.v1.QuizService/GetPersonalityById', - request_serializer=bionode__comm__pb2.IdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.PersonalityRes.FromString, - _registered_method=True) - self.ListPersonalities = channel.unary_unary( - '/bionode.comm.v1.QuizService/ListPersonalities', - request_serializer=bionode__comm__pb2.ListPersonalitiesReq.SerializeToString, - response_deserializer=bionode__comm__pb2.PersonalityListRes.FromString, - _registered_method=True) - self.CreatePersonality = channel.unary_unary( - '/bionode.comm.v1.QuizService/CreatePersonality', - request_serializer=bionode__comm__pb2.CreatePersonalityReq.SerializeToString, - response_deserializer=bionode__comm__pb2.PersonalityRes.FromString, - _registered_method=True) - self.UpdatePersonality = channel.unary_unary( - '/bionode.comm.v1.QuizService/UpdatePersonality', - request_serializer=bionode__comm__pb2.UpdatePersonalityReq.SerializeToString, - response_deserializer=bionode__comm__pb2.PersonalityRes.FromString, - _registered_method=True) - self.UpdatePersonalityStatus = channel.unary_unary( - '/bionode.comm.v1.QuizService/UpdatePersonalityStatus', - request_serializer=bionode__comm__pb2.UpdatePersonalityStatusReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.UpdatePersonalityPeriods = channel.unary_unary( - '/bionode.comm.v1.QuizService/UpdatePersonalityPeriods', - request_serializer=bionode__comm__pb2.UpdatePersonalityPeriodsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.DeletePersonality = channel.unary_unary( - '/bionode.comm.v1.QuizService/DeletePersonality', - request_serializer=bionode__comm__pb2.IdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.GetDistinctMhrCodes = channel.unary_unary( - '/bionode.comm.v1.QuizService/GetDistinctMhrCodes', - request_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - response_deserializer=bionode__comm__pb2.MhrCodesRes.FromString, - _registered_method=True) - - -class QuizServiceServicer: - """============================================================ - Comm Service — 通用业务微服务 - 端口: gRPC :50062 - 包含: QuizService / SurveyService / ShareService / EventService - ============================================================ - - ==================== QuizService ==================== - 题库管理(全局题目池) - 对应表: quiz_questions - - """ - - def GetActiveQuestions(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListQuestions(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetQuestion(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def CreateQuestion(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpdateQuestion(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpdateQuestionStatus(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def DeleteQuestion(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetQuestionsByIds(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetPersonality(self, request, context): - """----- 人格配置 ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetPersonalityById(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListPersonalities(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def CreatePersonality(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpdatePersonality(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpdatePersonalityStatus(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpdatePersonalityPeriods(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def DeletePersonality(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetDistinctMhrCodes(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_QuizServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'GetActiveQuestions': grpc.unary_unary_rpc_method_handler( - servicer.GetActiveQuestions, - request_deserializer=bionode__comm__pb2.GetActiveQuestionsReq.FromString, - response_serializer=bionode__comm__pb2.QuestionListRes.SerializeToString, - ), - 'ListQuestions': grpc.unary_unary_rpc_method_handler( - servicer.ListQuestions, - request_deserializer=bionode__comm__pb2.ListQuestionsReq.FromString, - response_serializer=bionode__comm__pb2.QuestionListRes.SerializeToString, - ), - 'GetQuestion': grpc.unary_unary_rpc_method_handler( - servicer.GetQuestion, - request_deserializer=bionode__comm__pb2.IdReq.FromString, - response_serializer=bionode__comm__pb2.QuestionRes.SerializeToString, - ), - 'CreateQuestion': grpc.unary_unary_rpc_method_handler( - servicer.CreateQuestion, - request_deserializer=bionode__comm__pb2.CreateQuestionReq.FromString, - response_serializer=bionode__comm__pb2.QuestionRes.SerializeToString, - ), - 'UpdateQuestion': grpc.unary_unary_rpc_method_handler( - servicer.UpdateQuestion, - request_deserializer=bionode__comm__pb2.UpdateQuestionReq.FromString, - response_serializer=bionode__comm__pb2.QuestionRes.SerializeToString, - ), - 'UpdateQuestionStatus': grpc.unary_unary_rpc_method_handler( - servicer.UpdateQuestionStatus, - request_deserializer=bionode__comm__pb2.UpdateQuestionStatusReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'DeleteQuestion': grpc.unary_unary_rpc_method_handler( - servicer.DeleteQuestion, - request_deserializer=bionode__comm__pb2.IdReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'GetQuestionsByIds': grpc.unary_unary_rpc_method_handler( - servicer.GetQuestionsByIds, - request_deserializer=bionode__comm__pb2.GetQuestionsByIdsRequest.FromString, - response_serializer=bionode__comm__pb2.QuestionListRes.SerializeToString, - ), - 'GetPersonality': grpc.unary_unary_rpc_method_handler( - servicer.GetPersonality, - request_deserializer=bionode__comm__pb2.GetPersonalityReq.FromString, - response_serializer=bionode__comm__pb2.PersonalityRes.SerializeToString, - ), - 'GetPersonalityById': grpc.unary_unary_rpc_method_handler( - servicer.GetPersonalityById, - request_deserializer=bionode__comm__pb2.IdReq.FromString, - response_serializer=bionode__comm__pb2.PersonalityRes.SerializeToString, - ), - 'ListPersonalities': grpc.unary_unary_rpc_method_handler( - servicer.ListPersonalities, - request_deserializer=bionode__comm__pb2.ListPersonalitiesReq.FromString, - response_serializer=bionode__comm__pb2.PersonalityListRes.SerializeToString, - ), - 'CreatePersonality': grpc.unary_unary_rpc_method_handler( - servicer.CreatePersonality, - request_deserializer=bionode__comm__pb2.CreatePersonalityReq.FromString, - response_serializer=bionode__comm__pb2.PersonalityRes.SerializeToString, - ), - 'UpdatePersonality': grpc.unary_unary_rpc_method_handler( - servicer.UpdatePersonality, - request_deserializer=bionode__comm__pb2.UpdatePersonalityReq.FromString, - response_serializer=bionode__comm__pb2.PersonalityRes.SerializeToString, - ), - 'UpdatePersonalityStatus': grpc.unary_unary_rpc_method_handler( - servicer.UpdatePersonalityStatus, - request_deserializer=bionode__comm__pb2.UpdatePersonalityStatusReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'UpdatePersonalityPeriods': grpc.unary_unary_rpc_method_handler( - servicer.UpdatePersonalityPeriods, - request_deserializer=bionode__comm__pb2.UpdatePersonalityPeriodsReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'DeletePersonality': grpc.unary_unary_rpc_method_handler( - servicer.DeletePersonality, - request_deserializer=bionode__comm__pb2.IdReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'GetDistinctMhrCodes': grpc.unary_unary_rpc_method_handler( - servicer.GetDistinctMhrCodes, - request_deserializer=bionode__comm__pb2.EmptyRes.FromString, - response_serializer=bionode__comm__pb2.MhrCodesRes.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'bionode.comm.v1.QuizService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('bionode.comm.v1.QuizService', rpc_method_handlers) - - - # This class is part of an EXPERIMENTAL API. -class QuizService: - """============================================================ - Comm Service — 通用业务微服务 - 端口: gRPC :50062 - 包含: QuizService / SurveyService / ShareService / EventService - ============================================================ - - ==================== QuizService ==================== - 题库管理(全局题目池) - 对应表: quiz_questions - - """ - - @staticmethod - def GetActiveQuestions(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/GetActiveQuestions', - bionode__comm__pb2.GetActiveQuestionsReq.SerializeToString, - bionode__comm__pb2.QuestionListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListQuestions(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/ListQuestions', - bionode__comm__pb2.ListQuestionsReq.SerializeToString, - bionode__comm__pb2.QuestionListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetQuestion(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/GetQuestion', - bionode__comm__pb2.IdReq.SerializeToString, - bionode__comm__pb2.QuestionRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def CreateQuestion(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/CreateQuestion', - bionode__comm__pb2.CreateQuestionReq.SerializeToString, - bionode__comm__pb2.QuestionRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpdateQuestion(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/UpdateQuestion', - bionode__comm__pb2.UpdateQuestionReq.SerializeToString, - bionode__comm__pb2.QuestionRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpdateQuestionStatus(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/UpdateQuestionStatus', - bionode__comm__pb2.UpdateQuestionStatusReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def DeleteQuestion(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/DeleteQuestion', - bionode__comm__pb2.IdReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetQuestionsByIds(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/GetQuestionsByIds', - bionode__comm__pb2.GetQuestionsByIdsRequest.SerializeToString, - bionode__comm__pb2.QuestionListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetPersonality(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/GetPersonality', - bionode__comm__pb2.GetPersonalityReq.SerializeToString, - bionode__comm__pb2.PersonalityRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetPersonalityById(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/GetPersonalityById', - bionode__comm__pb2.IdReq.SerializeToString, - bionode__comm__pb2.PersonalityRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListPersonalities(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/ListPersonalities', - bionode__comm__pb2.ListPersonalitiesReq.SerializeToString, - bionode__comm__pb2.PersonalityListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def CreatePersonality(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/CreatePersonality', - bionode__comm__pb2.CreatePersonalityReq.SerializeToString, - bionode__comm__pb2.PersonalityRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpdatePersonality(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/UpdatePersonality', - bionode__comm__pb2.UpdatePersonalityReq.SerializeToString, - bionode__comm__pb2.PersonalityRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpdatePersonalityStatus(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/UpdatePersonalityStatus', - bionode__comm__pb2.UpdatePersonalityStatusReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpdatePersonalityPeriods(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/UpdatePersonalityPeriods', - bionode__comm__pb2.UpdatePersonalityPeriodsReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def DeletePersonality(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/DeletePersonality', - bionode__comm__pb2.IdReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetDistinctMhrCodes(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.QuizService/GetDistinctMhrCodes', - bionode__comm__pb2.EmptyRes.SerializeToString, - bionode__comm__pb2.MhrCodesRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - -class SurveyServiceStub: - """==================== SurveyService ==================== - 问卷编排 + 答题 + 结果 - 对应表: quiz_surveys, quiz_answers, quiz_results - - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.GetSurvey = channel.unary_unary( - '/bionode.comm.v1.SurveyService/GetSurvey', - request_serializer=bionode__comm__pb2.GetSurveyReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SurveyRes.FromString, - _registered_method=True) - self.GetSurveyByCode = channel.unary_unary( - '/bionode.comm.v1.SurveyService/GetSurveyByCode', - request_serializer=bionode__comm__pb2.GetSurveyByCodeReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SurveyRes.FromString, - _registered_method=True) - self.ListSurveys = channel.unary_unary( - '/bionode.comm.v1.SurveyService/ListSurveys', - request_serializer=bionode__comm__pb2.ListSurveysReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SurveyListRes.FromString, - _registered_method=True) - self.CreateSurvey = channel.unary_unary( - '/bionode.comm.v1.SurveyService/CreateSurvey', - request_serializer=bionode__comm__pb2.CreateSurveyReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SurveyRes.FromString, - _registered_method=True) - self.UpdateSurvey = channel.unary_unary( - '/bionode.comm.v1.SurveyService/UpdateSurvey', - request_serializer=bionode__comm__pb2.UpdateSurveyReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SurveyRes.FromString, - _registered_method=True) - self.UpdateSurveyStatus = channel.unary_unary( - '/bionode.comm.v1.SurveyService/UpdateSurveyStatus', - request_serializer=bionode__comm__pb2.UpdateSurveyStatusReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.DeleteSurvey = channel.unary_unary( - '/bionode.comm.v1.SurveyService/DeleteSurvey', - request_serializer=bionode__comm__pb2.IdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.GetSurveyWithQuestions = channel.unary_unary( - '/bionode.comm.v1.SurveyService/GetSurveyWithQuestions', - request_serializer=bionode__comm__pb2.GetSurveyByCodeReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SurveyWithQuestionsRes.FromString, - _registered_method=True) - self.SaveAnswer = channel.unary_unary( - '/bionode.comm.v1.SurveyService/SaveAnswer', - request_serializer=bionode__comm__pb2.SaveAnswerReq.SerializeToString, - response_deserializer=bionode__comm__pb2.AnswerRes.FromString, - _registered_method=True) - self.SaveAnswerAlways = channel.unary_unary( - '/bionode.comm.v1.SurveyService/SaveAnswerAlways', - request_serializer=bionode__comm__pb2.SaveAnswerReq.SerializeToString, - response_deserializer=bionode__comm__pb2.AnswerRes.FromString, - _registered_method=True) - self.GetAnswerProgress = channel.unary_unary( - '/bionode.comm.v1.SurveyService/GetAnswerProgress', - request_serializer=bionode__comm__pb2.GetAnswerProgressReq.SerializeToString, - response_deserializer=bionode__comm__pb2.AnswerRes.FromString, - _registered_method=True) - self.ListAnswers = channel.unary_unary( - '/bionode.comm.v1.SurveyService/ListAnswers', - request_serializer=bionode__comm__pb2.ListAnswersReq.SerializeToString, - response_deserializer=bionode__comm__pb2.AnswerListRes.FromString, - _registered_method=True) - self.GetAnswerById = channel.unary_unary( - '/bionode.comm.v1.SurveyService/GetAnswerById', - request_serializer=bionode__comm__pb2.GetAnswerByIdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.AnswerRes.FromString, - _registered_method=True) - self.ComputeMhrCodes = channel.unary_unary( - '/bionode.comm.v1.SurveyService/ComputeMhrCodes', - request_serializer=bionode__comm__pb2.ComputeMhrCodesReq.SerializeToString, - response_deserializer=bionode__comm__pb2.ComputeMhrCodesRes.FromString, - _registered_method=True) - self.SubmitQuiz = channel.unary_unary( - '/bionode.comm.v1.SurveyService/SubmitQuiz', - request_serializer=bionode__comm__pb2.SubmitQuizReq.SerializeToString, - response_deserializer=bionode__comm__pb2.QuizResultRes.FromString, - _registered_method=True) - self.H5SubmitQuiz = channel.unary_unary( - '/bionode.comm.v1.SurveyService/H5SubmitQuiz', - request_serializer=bionode__comm__pb2.SubmitQuizReq.SerializeToString, - response_deserializer=bionode__comm__pb2.QuizResultRes.FromString, - _registered_method=True) - self.SubmitScaleQuiz = channel.unary_unary( - '/bionode.comm.v1.SurveyService/SubmitScaleQuiz', - request_serializer=bionode__comm__pb2.SubmitScaleQuizReq.SerializeToString, - response_deserializer=bionode__comm__pb2.QuizResultRes.FromString, - _registered_method=True) - self.GetLatestResult = channel.unary_unary( - '/bionode.comm.v1.SurveyService/GetLatestResult', - request_serializer=bionode__comm__pb2.GetLatestResultReq.SerializeToString, - response_deserializer=bionode__comm__pb2.QuizResultRes.FromString, - _registered_method=True) - self.GetResultById = channel.unary_unary( - '/bionode.comm.v1.SurveyService/GetResultById', - request_serializer=bionode__comm__pb2.GetResultByIdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.QuizResultRes.FromString, - _registered_method=True) - self.ListResults = channel.unary_unary( - '/bionode.comm.v1.SurveyService/ListResults', - request_serializer=bionode__comm__pb2.ListResultsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.QuizResultListRes.FromString, - _registered_method=True) - - -class SurveyServiceServicer: - """==================== SurveyService ==================== - 问卷编排 + 答题 + 结果 - 对应表: quiz_surveys, quiz_answers, quiz_results - - """ - - def GetSurvey(self, request, context): - """----- 问卷编排 ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetSurveyByCode(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListSurveys(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def CreateSurvey(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpdateSurvey(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpdateSurveyStatus(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def DeleteSurvey(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetSurveyWithQuestions(self, request, context): - """----- 获取问卷完整题目(H5 拉取用) ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def SaveAnswer(self, request, context): - """----- 答题 ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def SaveAnswerAlways(self, request, context): - """不按天去重,每次新建(App/Somni 用) - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetAnswerProgress(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListAnswers(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetAnswerById(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ComputeMhrCodes(self, request, context): - """----- 测评结果 ----- - 仅计算人格编码,不落库(Somni 匿名提交用) - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def SubmitQuiz(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def H5SubmitQuiz(self, request, context): - """H5:三位人格精确匹配 - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def SubmitScaleQuiz(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetLatestResult(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetResultById(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListResults(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_SurveyServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'GetSurvey': grpc.unary_unary_rpc_method_handler( - servicer.GetSurvey, - request_deserializer=bionode__comm__pb2.GetSurveyReq.FromString, - response_serializer=bionode__comm__pb2.SurveyRes.SerializeToString, - ), - 'GetSurveyByCode': grpc.unary_unary_rpc_method_handler( - servicer.GetSurveyByCode, - request_deserializer=bionode__comm__pb2.GetSurveyByCodeReq.FromString, - response_serializer=bionode__comm__pb2.SurveyRes.SerializeToString, - ), - 'ListSurveys': grpc.unary_unary_rpc_method_handler( - servicer.ListSurveys, - request_deserializer=bionode__comm__pb2.ListSurveysReq.FromString, - response_serializer=bionode__comm__pb2.SurveyListRes.SerializeToString, - ), - 'CreateSurvey': grpc.unary_unary_rpc_method_handler( - servicer.CreateSurvey, - request_deserializer=bionode__comm__pb2.CreateSurveyReq.FromString, - response_serializer=bionode__comm__pb2.SurveyRes.SerializeToString, - ), - 'UpdateSurvey': grpc.unary_unary_rpc_method_handler( - servicer.UpdateSurvey, - request_deserializer=bionode__comm__pb2.UpdateSurveyReq.FromString, - response_serializer=bionode__comm__pb2.SurveyRes.SerializeToString, - ), - 'UpdateSurveyStatus': grpc.unary_unary_rpc_method_handler( - servicer.UpdateSurveyStatus, - request_deserializer=bionode__comm__pb2.UpdateSurveyStatusReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'DeleteSurvey': grpc.unary_unary_rpc_method_handler( - servicer.DeleteSurvey, - request_deserializer=bionode__comm__pb2.IdReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'GetSurveyWithQuestions': grpc.unary_unary_rpc_method_handler( - servicer.GetSurveyWithQuestions, - request_deserializer=bionode__comm__pb2.GetSurveyByCodeReq.FromString, - response_serializer=bionode__comm__pb2.SurveyWithQuestionsRes.SerializeToString, - ), - 'SaveAnswer': grpc.unary_unary_rpc_method_handler( - servicer.SaveAnswer, - request_deserializer=bionode__comm__pb2.SaveAnswerReq.FromString, - response_serializer=bionode__comm__pb2.AnswerRes.SerializeToString, - ), - 'SaveAnswerAlways': grpc.unary_unary_rpc_method_handler( - servicer.SaveAnswerAlways, - request_deserializer=bionode__comm__pb2.SaveAnswerReq.FromString, - response_serializer=bionode__comm__pb2.AnswerRes.SerializeToString, - ), - 'GetAnswerProgress': grpc.unary_unary_rpc_method_handler( - servicer.GetAnswerProgress, - request_deserializer=bionode__comm__pb2.GetAnswerProgressReq.FromString, - response_serializer=bionode__comm__pb2.AnswerRes.SerializeToString, - ), - 'ListAnswers': grpc.unary_unary_rpc_method_handler( - servicer.ListAnswers, - request_deserializer=bionode__comm__pb2.ListAnswersReq.FromString, - response_serializer=bionode__comm__pb2.AnswerListRes.SerializeToString, - ), - 'GetAnswerById': grpc.unary_unary_rpc_method_handler( - servicer.GetAnswerById, - request_deserializer=bionode__comm__pb2.GetAnswerByIdReq.FromString, - response_serializer=bionode__comm__pb2.AnswerRes.SerializeToString, - ), - 'ComputeMhrCodes': grpc.unary_unary_rpc_method_handler( - servicer.ComputeMhrCodes, - request_deserializer=bionode__comm__pb2.ComputeMhrCodesReq.FromString, - response_serializer=bionode__comm__pb2.ComputeMhrCodesRes.SerializeToString, - ), - 'SubmitQuiz': grpc.unary_unary_rpc_method_handler( - servicer.SubmitQuiz, - request_deserializer=bionode__comm__pb2.SubmitQuizReq.FromString, - response_serializer=bionode__comm__pb2.QuizResultRes.SerializeToString, - ), - 'H5SubmitQuiz': grpc.unary_unary_rpc_method_handler( - servicer.H5SubmitQuiz, - request_deserializer=bionode__comm__pb2.SubmitQuizReq.FromString, - response_serializer=bionode__comm__pb2.QuizResultRes.SerializeToString, - ), - 'SubmitScaleQuiz': grpc.unary_unary_rpc_method_handler( - servicer.SubmitScaleQuiz, - request_deserializer=bionode__comm__pb2.SubmitScaleQuizReq.FromString, - response_serializer=bionode__comm__pb2.QuizResultRes.SerializeToString, - ), - 'GetLatestResult': grpc.unary_unary_rpc_method_handler( - servicer.GetLatestResult, - request_deserializer=bionode__comm__pb2.GetLatestResultReq.FromString, - response_serializer=bionode__comm__pb2.QuizResultRes.SerializeToString, - ), - 'GetResultById': grpc.unary_unary_rpc_method_handler( - servicer.GetResultById, - request_deserializer=bionode__comm__pb2.GetResultByIdReq.FromString, - response_serializer=bionode__comm__pb2.QuizResultRes.SerializeToString, - ), - 'ListResults': grpc.unary_unary_rpc_method_handler( - servicer.ListResults, - request_deserializer=bionode__comm__pb2.ListResultsReq.FromString, - response_serializer=bionode__comm__pb2.QuizResultListRes.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'bionode.comm.v1.SurveyService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('bionode.comm.v1.SurveyService', rpc_method_handlers) - - - # This class is part of an EXPERIMENTAL API. -class SurveyService: - """==================== SurveyService ==================== - 问卷编排 + 答题 + 结果 - 对应表: quiz_surveys, quiz_answers, quiz_results - - """ - - @staticmethod - def GetSurvey(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/GetSurvey', - bionode__comm__pb2.GetSurveyReq.SerializeToString, - bionode__comm__pb2.SurveyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetSurveyByCode(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/GetSurveyByCode', - bionode__comm__pb2.GetSurveyByCodeReq.SerializeToString, - bionode__comm__pb2.SurveyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListSurveys(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/ListSurveys', - bionode__comm__pb2.ListSurveysReq.SerializeToString, - bionode__comm__pb2.SurveyListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def CreateSurvey(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/CreateSurvey', - bionode__comm__pb2.CreateSurveyReq.SerializeToString, - bionode__comm__pb2.SurveyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpdateSurvey(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/UpdateSurvey', - bionode__comm__pb2.UpdateSurveyReq.SerializeToString, - bionode__comm__pb2.SurveyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpdateSurveyStatus(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/UpdateSurveyStatus', - bionode__comm__pb2.UpdateSurveyStatusReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def DeleteSurvey(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/DeleteSurvey', - bionode__comm__pb2.IdReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetSurveyWithQuestions(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/GetSurveyWithQuestions', - bionode__comm__pb2.GetSurveyByCodeReq.SerializeToString, - bionode__comm__pb2.SurveyWithQuestionsRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def SaveAnswer(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/SaveAnswer', - bionode__comm__pb2.SaveAnswerReq.SerializeToString, - bionode__comm__pb2.AnswerRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def SaveAnswerAlways(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/SaveAnswerAlways', - bionode__comm__pb2.SaveAnswerReq.SerializeToString, - bionode__comm__pb2.AnswerRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetAnswerProgress(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/GetAnswerProgress', - bionode__comm__pb2.GetAnswerProgressReq.SerializeToString, - bionode__comm__pb2.AnswerRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListAnswers(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/ListAnswers', - bionode__comm__pb2.ListAnswersReq.SerializeToString, - bionode__comm__pb2.AnswerListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetAnswerById(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/GetAnswerById', - bionode__comm__pb2.GetAnswerByIdReq.SerializeToString, - bionode__comm__pb2.AnswerRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ComputeMhrCodes(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/ComputeMhrCodes', - bionode__comm__pb2.ComputeMhrCodesReq.SerializeToString, - bionode__comm__pb2.ComputeMhrCodesRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def SubmitQuiz(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/SubmitQuiz', - bionode__comm__pb2.SubmitQuizReq.SerializeToString, - bionode__comm__pb2.QuizResultRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def H5SubmitQuiz(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/H5SubmitQuiz', - bionode__comm__pb2.SubmitQuizReq.SerializeToString, - bionode__comm__pb2.QuizResultRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def SubmitScaleQuiz(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/SubmitScaleQuiz', - bionode__comm__pb2.SubmitScaleQuizReq.SerializeToString, - bionode__comm__pb2.QuizResultRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetLatestResult(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/GetLatestResult', - bionode__comm__pb2.GetLatestResultReq.SerializeToString, - bionode__comm__pb2.QuizResultRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetResultById(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/GetResultById', - bionode__comm__pb2.GetResultByIdReq.SerializeToString, - bionode__comm__pb2.QuizResultRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListResults(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SurveyService/ListResults', - bionode__comm__pb2.ListResultsReq.SerializeToString, - bionode__comm__pb2.QuizResultListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - -class ShareServiceStub: - """==================== ShareService ==================== - 分享追踪 - 对应表: shares, share_visits - - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.CreateShare = channel.unary_unary( - '/bionode.comm.v1.ShareService/CreateShare', - request_serializer=bionode__comm__pb2.CreateShareReq.SerializeToString, - response_deserializer=bionode__comm__pb2.ShareRes.FromString, - _registered_method=True) - self.GetShareLanding = channel.unary_unary( - '/bionode.comm.v1.ShareService/GetShareLanding', - request_serializer=bionode__comm__pb2.GetShareLandingReq.SerializeToString, - response_deserializer=bionode__comm__pb2.ShareLandingRes.FromString, - _registered_method=True) - self.RecordVisit = channel.unary_unary( - '/bionode.comm.v1.ShareService/RecordVisit', - request_serializer=bionode__comm__pb2.RecordVisitReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.ListShareTracking = channel.unary_unary( - '/bionode.comm.v1.ShareService/ListShareTracking', - request_serializer=bionode__comm__pb2.ListShareTrackingReq.SerializeToString, - response_deserializer=bionode__comm__pb2.ShareTrackingListRes.FromString, - _registered_method=True) - self.ListSharerRanking = channel.unary_unary( - '/bionode.comm.v1.ShareService/ListSharerRanking', - request_serializer=bionode__comm__pb2.ListSharerRankingReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SharerRankingListRes.FromString, - _registered_method=True) - self.ListSharesByUser = channel.unary_unary( - '/bionode.comm.v1.ShareService/ListSharesByUser', - request_serializer=bionode__comm__pb2.ListSharesByUserReq.SerializeToString, - response_deserializer=bionode__comm__pb2.ShareListRes.FromString, - _registered_method=True) - self.ListShareVisits = channel.unary_unary( - '/bionode.comm.v1.ShareService/ListShareVisits', - request_serializer=bionode__comm__pb2.ListShareVisitsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.ShareVisitListRes.FromString, - _registered_method=True) - - -class ShareServiceServicer: - """==================== ShareService ==================== - 分享追踪 - 对应表: shares, share_visits - - """ - - def CreateShare(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetShareLanding(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def RecordVisit(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListShareTracking(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListSharerRanking(self, request, context): - """----- Admin 运营管理 ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListSharesByUser(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListShareVisits(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_ShareServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'CreateShare': grpc.unary_unary_rpc_method_handler( - servicer.CreateShare, - request_deserializer=bionode__comm__pb2.CreateShareReq.FromString, - response_serializer=bionode__comm__pb2.ShareRes.SerializeToString, - ), - 'GetShareLanding': grpc.unary_unary_rpc_method_handler( - servicer.GetShareLanding, - request_deserializer=bionode__comm__pb2.GetShareLandingReq.FromString, - response_serializer=bionode__comm__pb2.ShareLandingRes.SerializeToString, - ), - 'RecordVisit': grpc.unary_unary_rpc_method_handler( - servicer.RecordVisit, - request_deserializer=bionode__comm__pb2.RecordVisitReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'ListShareTracking': grpc.unary_unary_rpc_method_handler( - servicer.ListShareTracking, - request_deserializer=bionode__comm__pb2.ListShareTrackingReq.FromString, - response_serializer=bionode__comm__pb2.ShareTrackingListRes.SerializeToString, - ), - 'ListSharerRanking': grpc.unary_unary_rpc_method_handler( - servicer.ListSharerRanking, - request_deserializer=bionode__comm__pb2.ListSharerRankingReq.FromString, - response_serializer=bionode__comm__pb2.SharerRankingListRes.SerializeToString, - ), - 'ListSharesByUser': grpc.unary_unary_rpc_method_handler( - servicer.ListSharesByUser, - request_deserializer=bionode__comm__pb2.ListSharesByUserReq.FromString, - response_serializer=bionode__comm__pb2.ShareListRes.SerializeToString, - ), - 'ListShareVisits': grpc.unary_unary_rpc_method_handler( - servicer.ListShareVisits, - request_deserializer=bionode__comm__pb2.ListShareVisitsReq.FromString, - response_serializer=bionode__comm__pb2.ShareVisitListRes.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'bionode.comm.v1.ShareService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('bionode.comm.v1.ShareService', rpc_method_handlers) - - - # This class is part of an EXPERIMENTAL API. -class ShareService: - """==================== ShareService ==================== - 分享追踪 - 对应表: shares, share_visits - - """ - - @staticmethod - def CreateShare(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.ShareService/CreateShare', - bionode__comm__pb2.CreateShareReq.SerializeToString, - bionode__comm__pb2.ShareRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetShareLanding(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.ShareService/GetShareLanding', - bionode__comm__pb2.GetShareLandingReq.SerializeToString, - bionode__comm__pb2.ShareLandingRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def RecordVisit(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.ShareService/RecordVisit', - bionode__comm__pb2.RecordVisitReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListShareTracking(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.ShareService/ListShareTracking', - bionode__comm__pb2.ListShareTrackingReq.SerializeToString, - bionode__comm__pb2.ShareTrackingListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListSharerRanking(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.ShareService/ListSharerRanking', - bionode__comm__pb2.ListSharerRankingReq.SerializeToString, - bionode__comm__pb2.SharerRankingListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListSharesByUser(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.ShareService/ListSharesByUser', - bionode__comm__pb2.ListSharesByUserReq.SerializeToString, - bionode__comm__pb2.ShareListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListShareVisits(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.ShareService/ListShareVisits', - bionode__comm__pb2.ListShareVisitsReq.SerializeToString, - bionode__comm__pb2.ShareVisitListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - -class EventServiceStub: - """==================== EventService ==================== - 埋点日志与数据统计 - 对应表: log_events - - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.TrackEvent = channel.unary_unary( - '/bionode.comm.v1.EventService/TrackEvent', - request_serializer=bionode__comm__pb2.TrackEventReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.BatchTrack = channel.unary_unary( - '/bionode.comm.v1.EventService/BatchTrack', - request_serializer=bionode__comm__pb2.BatchTrackReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.GetOverview = channel.unary_unary( - '/bionode.comm.v1.EventService/GetOverview', - request_serializer=bionode__comm__pb2.GetOverviewReq.SerializeToString, - response_deserializer=bionode__comm__pb2.OverviewRes.FromString, - _registered_method=True) - self.GetPageViewUV = channel.unary_unary( - '/bionode.comm.v1.EventService/GetPageViewUV', - request_serializer=bionode__comm__pb2.GetPageViewUVReq.SerializeToString, - response_deserializer=bionode__comm__pb2.CountRes.FromString, - _registered_method=True) - self.GetQuizCompleteUV = channel.unary_unary( - '/bionode.comm.v1.EventService/GetQuizCompleteUV', - request_serializer=bionode__comm__pb2.GetQuizCompleteUVReq.SerializeToString, - response_deserializer=bionode__comm__pb2.CountRes.FromString, - _registered_method=True) - self.GetInteractionCount = channel.unary_unary( - '/bionode.comm.v1.EventService/GetInteractionCount', - request_serializer=bionode__comm__pb2.GetInteractionCountReq.SerializeToString, - response_deserializer=bionode__comm__pb2.CountRes.FromString, - _registered_method=True) - self.ListLogEvents = channel.unary_unary( - '/bionode.comm.v1.EventService/ListLogEvents', - request_serializer=bionode__comm__pb2.ListLogEventsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.ListLogEventsRes.FromString, - _registered_method=True) - self.GetEventTrend = channel.unary_unary( - '/bionode.comm.v1.EventService/GetEventTrend', - request_serializer=bionode__comm__pb2.GetEventTrendReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EventTrendRes.FromString, - _registered_method=True) - self.GetEventFilterOptions = channel.unary_unary( - '/bionode.comm.v1.EventService/GetEventFilterOptions', - request_serializer=bionode__comm__pb2.GetEventFilterOptionsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EventFilterOptionsRes.FromString, - _registered_method=True) - - -class EventServiceServicer: - """==================== EventService ==================== - 埋点日志与数据统计 - 对应表: log_events - - """ - - def TrackEvent(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def BatchTrack(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetOverview(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetPageViewUV(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetQuizCompleteUV(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetInteractionCount(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListLogEvents(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetEventTrend(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetEventFilterOptions(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_EventServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'TrackEvent': grpc.unary_unary_rpc_method_handler( - servicer.TrackEvent, - request_deserializer=bionode__comm__pb2.TrackEventReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'BatchTrack': grpc.unary_unary_rpc_method_handler( - servicer.BatchTrack, - request_deserializer=bionode__comm__pb2.BatchTrackReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'GetOverview': grpc.unary_unary_rpc_method_handler( - servicer.GetOverview, - request_deserializer=bionode__comm__pb2.GetOverviewReq.FromString, - response_serializer=bionode__comm__pb2.OverviewRes.SerializeToString, - ), - 'GetPageViewUV': grpc.unary_unary_rpc_method_handler( - servicer.GetPageViewUV, - request_deserializer=bionode__comm__pb2.GetPageViewUVReq.FromString, - response_serializer=bionode__comm__pb2.CountRes.SerializeToString, - ), - 'GetQuizCompleteUV': grpc.unary_unary_rpc_method_handler( - servicer.GetQuizCompleteUV, - request_deserializer=bionode__comm__pb2.GetQuizCompleteUVReq.FromString, - response_serializer=bionode__comm__pb2.CountRes.SerializeToString, - ), - 'GetInteractionCount': grpc.unary_unary_rpc_method_handler( - servicer.GetInteractionCount, - request_deserializer=bionode__comm__pb2.GetInteractionCountReq.FromString, - response_serializer=bionode__comm__pb2.CountRes.SerializeToString, - ), - 'ListLogEvents': grpc.unary_unary_rpc_method_handler( - servicer.ListLogEvents, - request_deserializer=bionode__comm__pb2.ListLogEventsReq.FromString, - response_serializer=bionode__comm__pb2.ListLogEventsRes.SerializeToString, - ), - 'GetEventTrend': grpc.unary_unary_rpc_method_handler( - servicer.GetEventTrend, - request_deserializer=bionode__comm__pb2.GetEventTrendReq.FromString, - response_serializer=bionode__comm__pb2.EventTrendRes.SerializeToString, - ), - 'GetEventFilterOptions': grpc.unary_unary_rpc_method_handler( - servicer.GetEventFilterOptions, - request_deserializer=bionode__comm__pb2.GetEventFilterOptionsReq.FromString, - response_serializer=bionode__comm__pb2.EventFilterOptionsRes.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'bionode.comm.v1.EventService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('bionode.comm.v1.EventService', rpc_method_handlers) - - - # This class is part of an EXPERIMENTAL API. -class EventService: - """==================== EventService ==================== - 埋点日志与数据统计 - 对应表: log_events - - """ - - @staticmethod - def TrackEvent(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.EventService/TrackEvent', - bionode__comm__pb2.TrackEventReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def BatchTrack(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.EventService/BatchTrack', - bionode__comm__pb2.BatchTrackReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetOverview(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.EventService/GetOverview', - bionode__comm__pb2.GetOverviewReq.SerializeToString, - bionode__comm__pb2.OverviewRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetPageViewUV(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.EventService/GetPageViewUV', - bionode__comm__pb2.GetPageViewUVReq.SerializeToString, - bionode__comm__pb2.CountRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetQuizCompleteUV(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.EventService/GetQuizCompleteUV', - bionode__comm__pb2.GetQuizCompleteUVReq.SerializeToString, - bionode__comm__pb2.CountRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetInteractionCount(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.EventService/GetInteractionCount', - bionode__comm__pb2.GetInteractionCountReq.SerializeToString, - bionode__comm__pb2.CountRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListLogEvents(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.EventService/ListLogEvents', - bionode__comm__pb2.ListLogEventsReq.SerializeToString, - bionode__comm__pb2.ListLogEventsRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetEventTrend(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.EventService/GetEventTrend', - bionode__comm__pb2.GetEventTrendReq.SerializeToString, - bionode__comm__pb2.EventTrendRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetEventFilterOptions(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.EventService/GetEventFilterOptions', - bionode__comm__pb2.GetEventFilterOptionsReq.SerializeToString, - bionode__comm__pb2.EventFilterOptionsRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - -class SomniServiceStub: - """==================== SomniService ==================== - 睡眠报告、方案、基线、融合数据 - 对应表: somni_*(报告、会话、方案、事件、生理、环境等) - - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.SetAlarm = channel.unary_unary( - '/bionode.comm.v1.SomniService/SetAlarm', - request_serializer=bionode__comm__pb2.SomniSetAlarmReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniSetAlarmRes.FromString, - _registered_method=True) - self.GetPlan = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetPlan', - request_serializer=bionode__comm__pb2.SomniGetPlanReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetPlanRes.FromString, - _registered_method=True) - self.MarkPlanStarted = channel.unary_unary( - '/bionode.comm.v1.SomniService/MarkPlanStarted', - request_serializer=bionode__comm__pb2.SomniMarkPlanStartedReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniMarkPlanStartedRes.FromString, - _registered_method=True) - self.MarkPlanStopped = channel.unary_unary( - '/bionode.comm.v1.SomniService/MarkPlanStopped', - request_serializer=bionode__comm__pb2.SomniMarkPlanStoppedReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniMarkPlanStoppedRes.FromString, - _registered_method=True) - self.StartSomniDeviceSession = channel.unary_unary( - '/bionode.comm.v1.SomniService/StartSomniDeviceSession', - request_serializer=bionode__comm__pb2.SomniStartSomniDeviceSessionReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniStartSomniDeviceSessionRes.FromString, - _registered_method=True) - self.StopSomniDeviceSession = channel.unary_unary( - '/bionode.comm.v1.SomniService/StopSomniDeviceSession', - request_serializer=bionode__comm__pb2.SomniStopSomniDeviceSessionReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniStopSomniDeviceSessionRes.FromString, - _registered_method=True) - self.GetFusion = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetFusion', - request_serializer=bionode__comm__pb2.SomniGetFusionReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetFusionRes.FromString, - _registered_method=True) - self.GetBaseline = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetBaseline', - request_serializer=bionode__comm__pb2.SomniGetBaselineReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetBaselineRes.FromString, - _registered_method=True) - self.GetReport = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetReport', - request_serializer=bionode__comm__pb2.SomniGetReportReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetReportRes.FromString, - _registered_method=True) - self.GetSleepPlanet = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetSleepPlanet', - request_serializer=bionode__comm__pb2.SomniGetSleepPlanetReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetSleepPlanetRes.FromString, - _registered_method=True) - self.ListSleepPlanets = channel.unary_unary( - '/bionode.comm.v1.SomniService/ListSleepPlanets', - request_serializer=bionode__comm__pb2.SomniListSleepPlanetsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniListSleepPlanetsRes.FromString, - _registered_method=True) - self.ValidateMonitorSession = channel.unary_unary( - '/bionode.comm.v1.SomniService/ValidateMonitorSession', - request_serializer=bionode__comm__pb2.SomniValidateMonitorSessionReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniValidateMonitorSessionRes.FromString, - _registered_method=True) - self.GetMonitorPayload = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetMonitorPayload', - request_serializer=bionode__comm__pb2.SomniGetMonitorPayloadReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetMonitorPayloadRes.FromString, - _registered_method=True) - self.GetUidByMhrCodes = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetUidByMhrCodes', - request_serializer=bionode__comm__pb2.SomniGetUidByMhrCodesReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetUidByMhrCodesRes.FromString, - _registered_method=True) - self.CreateQuizSession = channel.unary_unary( - '/bionode.comm.v1.SomniService/CreateQuizSession', - request_serializer=bionode__comm__pb2.SomniCreateQuizSessionReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniCreateQuizSessionRes.FromString, - _registered_method=True) - self.UpsertSomniAppInstUser = channel.unary_unary( - '/bionode.comm.v1.SomniService/UpsertSomniAppInstUser', - request_serializer=bionode__comm__pb2.SomniUpsertSomniAppInstUserReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniUpsertSomniAppInstUserRes.FromString, - _registered_method=True) - self.DeleteSomniAppInstUser = channel.unary_unary( - '/bionode.comm.v1.SomniService/DeleteSomniAppInstUser', - request_serializer=bionode__comm__pb2.SomniDeleteSomniAppInstUserReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniUpsertSomniAppInstUserRes.FromString, - _registered_method=True) - self.SomniQuizChat = channel.unary_unary( - '/bionode.comm.v1.SomniService/SomniQuizChat', - request_serializer=bionode__comm__pb2.SomniQuizChatReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniQuizChatRes.FromString, - _registered_method=True) - self.ListInterventionConfigs = channel.unary_unary( - '/bionode.comm.v1.SomniService/ListInterventionConfigs', - request_serializer=bionode__comm__pb2.ListInterventionConfigsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.InterventionConfigListRes.FromString, - _registered_method=True) - self.GetInterventionConfig = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetInterventionConfig', - request_serializer=bionode__comm__pb2.IdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.InterventionConfigRes.FromString, - _registered_method=True) - self.UpsertInterventionConfig = channel.unary_unary( - '/bionode.comm.v1.SomniService/UpsertInterventionConfig', - request_serializer=bionode__comm__pb2.UpsertInterventionConfigReq.SerializeToString, - response_deserializer=bionode__comm__pb2.InterventionConfigRes.FromString, - _registered_method=True) - self.DeleteInterventionConfig = channel.unary_unary( - '/bionode.comm.v1.SomniService/DeleteInterventionConfig', - request_serializer=bionode__comm__pb2.IdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.GetPlanPhases = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetPlanPhases', - request_serializer=bionode__comm__pb2.SomniGetPlanPhasesReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetPlanPhasesRes.FromString, - _registered_method=True) - self.GetInterventionEvents = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetInterventionEvents', - request_serializer=bionode__comm__pb2.SomniGetInterventionEventsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetInterventionEventsRes.FromString, - _registered_method=True) - self.GetMonitorReportData = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetMonitorReportData', - request_serializer=bionode__comm__pb2.SomniGetMonitorReportDataReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetMonitorReportDataRes.FromString, - _registered_method=True) - self.GenerateMonitorDphReports = channel.unary_unary( - '/bionode.comm.v1.SomniService/GenerateMonitorDphReports', - request_serializer=bionode__comm__pb2.SomniGenerateMonitorDphReportsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGenerateMonitorDphReportsRes.FromString, - _registered_method=True) - self.GetSessionContext = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetSessionContext', - request_serializer=bionode__comm__pb2.SomniGetSessionContextReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetSessionContextRes.FromString, - _registered_method=True) - self.ConsoleStopDeviceSession = channel.unary_unary( - '/bionode.comm.v1.SomniService/ConsoleStopDeviceSession', - request_serializer=bionode__comm__pb2.SomniConsoleStopDeviceSessionReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniConsoleStopDeviceSessionRes.FromString, - _registered_method=True) - self.TriggerIntervention = channel.unary_unary( - '/bionode.comm.v1.SomniService/TriggerIntervention', - request_serializer=bionode__comm__pb2.TriggerInterventionReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.StopIntervention = channel.unary_unary( - '/bionode.comm.v1.SomniService/StopIntervention', - request_serializer=bionode__comm__pb2.StopInterventionReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.ResetAiThoughtRehearsalState = channel.unary_unary( - '/bionode.comm.v1.SomniService/ResetAiThoughtRehearsalState', - request_serializer=bionode__comm__pb2.ResetAiThoughtRehearsalStateReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.EmitDeviceLinkAiThought = channel.unary_unary( - '/bionode.comm.v1.SomniService/EmitDeviceLinkAiThought', - request_serializer=bionode__comm__pb2.EmitDeviceLinkAiThoughtReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.GetChatMessages = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetChatMessages', - request_serializer=bionode__comm__pb2.GetChatMessagesReq.SerializeToString, - response_deserializer=bionode__comm__pb2.ChatMessageListRes.FromString, - _registered_method=True) - self.ListChatConversations = channel.unary_unary( - '/bionode.comm.v1.SomniService/ListChatConversations', - request_serializer=bionode__comm__pb2.ListChatConversationsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.ChatConversationListRes.FromString, - _registered_method=True) - self.ListUserChatConversations = channel.unary_unary( - '/bionode.comm.v1.SomniService/ListUserChatConversations', - request_serializer=bionode__comm__pb2.ListUserChatConversationsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.ChatConversationListRes.FromString, - _registered_method=True) - self.GetTempPlan = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetTempPlan', - request_serializer=bionode__comm__pb2.SomniGetTempPlanReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetTempPlanRes.FromString, - _registered_method=True) - self.UploadChatAudio = channel.unary_unary( - '/bionode.comm.v1.SomniService/UploadChatAudio', - request_serializer=bionode__comm__pb2.UploadChatAudioReq.SerializeToString, - response_deserializer=bionode__comm__pb2.UploadChatAudioRes.FromString, - _registered_method=True) - self.AppendChatMessage = channel.unary_unary( - '/bionode.comm.v1.SomniService/AppendChatMessage', - request_serializer=bionode__comm__pb2.AppendChatMessageReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.GetSleepMapCityOverview = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetSleepMapCityOverview', - request_serializer=bionode__comm__pb2.SomniGetSleepMapCityOverviewReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetSleepMapCityOverviewRes.FromString, - _registered_method=True) - self.GetSleepMapHighlights = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetSleepMapHighlights', - request_serializer=bionode__comm__pb2.SomniGetSleepMapHighlightsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetSleepMapHighlightsRes.FromString, - _registered_method=True) - self.GetSleepMapCityRanking = channel.unary_unary( - '/bionode.comm.v1.SomniService/GetSleepMapCityRanking', - request_serializer=bionode__comm__pb2.SomniGetSleepMapCityRankingReq.SerializeToString, - response_deserializer=bionode__comm__pb2.SomniGetSleepMapCityRankingRes.FromString, - _registered_method=True) - self.CreateDphConsoleSession = channel.unary_unary( - '/bionode.comm.v1.SomniService/CreateDphConsoleSession', - request_serializer=bionode__comm__pb2.CreateDphConsoleSessionReq.SerializeToString, - response_deserializer=bionode__comm__pb2.CreateDphConsoleSessionRes.FromString, - _registered_method=True) - - -class SomniServiceServicer: - """==================== SomniService ==================== - 睡眠报告、方案、基线、融合数据 - 对应表: somni_*(报告、会话、方案、事件、生理、环境等) - - """ - - def SetAlarm(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetPlan(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def MarkPlanStarted(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def MarkPlanStopped(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def StartSomniDeviceSession(self, request, context): - """* App 伴睡开始:comm 编排绑定设备 + device-gateway StartSession(scheme_v1 → MQTT 在网关转换) - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def StopSomniDeviceSession(self, request, context): - """* App 伴睡结束:停会话、解绑 - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetFusion(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetBaseline(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetReport(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetSleepPlanet(self, request, context): - """* 睡眠星球详情:record_date 空则按服务端当日(UTC 日历,与 GetReport 一致) - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListSleepPlanets(self, request, context): - """* 睡眠星球列表,按 record_date 倒序分页 - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ValidateMonitorSession(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetMonitorPayload(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetUidByMhrCodes(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def CreateQuizSession(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpsertSomniAppInstUser(self, request, context): - """* 问卷提交后:Redis 覆盖当前 X-App-Instance-Id 对应的映射 uid - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def DeleteSomniAppInstUser(self, request, context): - """* 设备解绑时:删除 Redis 中该实例的 quiz_uid 映射 - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def SomniQuizChat(self, request, context): - """* Somni Quiz AI 对话问卷(comm 编排 Init/Chat、Redis 快照;finalized 后等价 submit 落库) - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListInterventionConfigs(self, request, context): - """----- 干预配置 CRUD ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetInterventionConfig(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpsertInterventionConfig(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def DeleteInterventionConfig(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetPlanPhases(self, request, context): - """----- 监控页查询接口 ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetInterventionEvents(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetMonitorReportData(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GenerateMonitorDphReports(self, request, context): - """* 监控端睡眠报告:调用 DPH ProcessRadarReport 生成真/假两份报告并写入 Redis 缓存 - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetSessionContext(self, request, context): - """----- 控制台会话上下文 ----- - - * 根据 session_id 查询会话关联的人格编码和方案阶段,供控制台接管 APP 会话时加载 - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ConsoleStopDeviceSession(self, request, context): - """* 控制台结束会话:停设备与调度;有 somni_plans 时同步 is_started=false - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def TriggerIntervention(self, request, context): - """----- 控制台手动干预 ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def StopIntervention(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ResetAiThoughtRehearsalState(self, request, context): - """* 联调:清空 AI 思维滑动窗/冷却/patrol/序列(不抑制 patrol,不清 current_phase) - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def EmitDeviceLinkAiThought(self, request, context): - """* 设备 MQTT 连断 — 推送 device_link 思维流(device stream,可无 session) - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetChatMessages(self, request, context): - """----- 对话记录查询 ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListChatConversations(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListUserChatConversations(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetTempPlan(self, request, context): - """----- 临时方案 ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UploadChatAudio(self, request, context): - """----- Somni 对话音频上传 ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def AppendChatMessage(self, request, context): - """----- Somni 对话消息落库 ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetSleepMapCityOverview(self, request, context): - """----- 睡眠地图(预设区级聚合 + 个人分析) ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetSleepMapHighlights(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetSleepMapCityRanking(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def CreateDphConsoleSession(self, request, context): - """----- DPH 控制台专用 ----- - - * 为 DPH 方案创建独立会话 + 方案记录(admin 控制台调用) - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_SomniServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'SetAlarm': grpc.unary_unary_rpc_method_handler( - servicer.SetAlarm, - request_deserializer=bionode__comm__pb2.SomniSetAlarmReq.FromString, - response_serializer=bionode__comm__pb2.SomniSetAlarmRes.SerializeToString, - ), - 'GetPlan': grpc.unary_unary_rpc_method_handler( - servicer.GetPlan, - request_deserializer=bionode__comm__pb2.SomniGetPlanReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetPlanRes.SerializeToString, - ), - 'MarkPlanStarted': grpc.unary_unary_rpc_method_handler( - servicer.MarkPlanStarted, - request_deserializer=bionode__comm__pb2.SomniMarkPlanStartedReq.FromString, - response_serializer=bionode__comm__pb2.SomniMarkPlanStartedRes.SerializeToString, - ), - 'MarkPlanStopped': grpc.unary_unary_rpc_method_handler( - servicer.MarkPlanStopped, - request_deserializer=bionode__comm__pb2.SomniMarkPlanStoppedReq.FromString, - response_serializer=bionode__comm__pb2.SomniMarkPlanStoppedRes.SerializeToString, - ), - 'StartSomniDeviceSession': grpc.unary_unary_rpc_method_handler( - servicer.StartSomniDeviceSession, - request_deserializer=bionode__comm__pb2.SomniStartSomniDeviceSessionReq.FromString, - response_serializer=bionode__comm__pb2.SomniStartSomniDeviceSessionRes.SerializeToString, - ), - 'StopSomniDeviceSession': grpc.unary_unary_rpc_method_handler( - servicer.StopSomniDeviceSession, - request_deserializer=bionode__comm__pb2.SomniStopSomniDeviceSessionReq.FromString, - response_serializer=bionode__comm__pb2.SomniStopSomniDeviceSessionRes.SerializeToString, - ), - 'GetFusion': grpc.unary_unary_rpc_method_handler( - servicer.GetFusion, - request_deserializer=bionode__comm__pb2.SomniGetFusionReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetFusionRes.SerializeToString, - ), - 'GetBaseline': grpc.unary_unary_rpc_method_handler( - servicer.GetBaseline, - request_deserializer=bionode__comm__pb2.SomniGetBaselineReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetBaselineRes.SerializeToString, - ), - 'GetReport': grpc.unary_unary_rpc_method_handler( - servicer.GetReport, - request_deserializer=bionode__comm__pb2.SomniGetReportReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetReportRes.SerializeToString, - ), - 'GetSleepPlanet': grpc.unary_unary_rpc_method_handler( - servicer.GetSleepPlanet, - request_deserializer=bionode__comm__pb2.SomniGetSleepPlanetReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetSleepPlanetRes.SerializeToString, - ), - 'ListSleepPlanets': grpc.unary_unary_rpc_method_handler( - servicer.ListSleepPlanets, - request_deserializer=bionode__comm__pb2.SomniListSleepPlanetsReq.FromString, - response_serializer=bionode__comm__pb2.SomniListSleepPlanetsRes.SerializeToString, - ), - 'ValidateMonitorSession': grpc.unary_unary_rpc_method_handler( - servicer.ValidateMonitorSession, - request_deserializer=bionode__comm__pb2.SomniValidateMonitorSessionReq.FromString, - response_serializer=bionode__comm__pb2.SomniValidateMonitorSessionRes.SerializeToString, - ), - 'GetMonitorPayload': grpc.unary_unary_rpc_method_handler( - servicer.GetMonitorPayload, - request_deserializer=bionode__comm__pb2.SomniGetMonitorPayloadReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetMonitorPayloadRes.SerializeToString, - ), - 'GetUidByMhrCodes': grpc.unary_unary_rpc_method_handler( - servicer.GetUidByMhrCodes, - request_deserializer=bionode__comm__pb2.SomniGetUidByMhrCodesReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetUidByMhrCodesRes.SerializeToString, - ), - 'CreateQuizSession': grpc.unary_unary_rpc_method_handler( - servicer.CreateQuizSession, - request_deserializer=bionode__comm__pb2.SomniCreateQuizSessionReq.FromString, - response_serializer=bionode__comm__pb2.SomniCreateQuizSessionRes.SerializeToString, - ), - 'UpsertSomniAppInstUser': grpc.unary_unary_rpc_method_handler( - servicer.UpsertSomniAppInstUser, - request_deserializer=bionode__comm__pb2.SomniUpsertSomniAppInstUserReq.FromString, - response_serializer=bionode__comm__pb2.SomniUpsertSomniAppInstUserRes.SerializeToString, - ), - 'DeleteSomniAppInstUser': grpc.unary_unary_rpc_method_handler( - servicer.DeleteSomniAppInstUser, - request_deserializer=bionode__comm__pb2.SomniDeleteSomniAppInstUserReq.FromString, - response_serializer=bionode__comm__pb2.SomniUpsertSomniAppInstUserRes.SerializeToString, - ), - 'SomniQuizChat': grpc.unary_unary_rpc_method_handler( - servicer.SomniQuizChat, - request_deserializer=bionode__comm__pb2.SomniQuizChatReq.FromString, - response_serializer=bionode__comm__pb2.SomniQuizChatRes.SerializeToString, - ), - 'ListInterventionConfigs': grpc.unary_unary_rpc_method_handler( - servicer.ListInterventionConfigs, - request_deserializer=bionode__comm__pb2.ListInterventionConfigsReq.FromString, - response_serializer=bionode__comm__pb2.InterventionConfigListRes.SerializeToString, - ), - 'GetInterventionConfig': grpc.unary_unary_rpc_method_handler( - servicer.GetInterventionConfig, - request_deserializer=bionode__comm__pb2.IdReq.FromString, - response_serializer=bionode__comm__pb2.InterventionConfigRes.SerializeToString, - ), - 'UpsertInterventionConfig': grpc.unary_unary_rpc_method_handler( - servicer.UpsertInterventionConfig, - request_deserializer=bionode__comm__pb2.UpsertInterventionConfigReq.FromString, - response_serializer=bionode__comm__pb2.InterventionConfigRes.SerializeToString, - ), - 'DeleteInterventionConfig': grpc.unary_unary_rpc_method_handler( - servicer.DeleteInterventionConfig, - request_deserializer=bionode__comm__pb2.IdReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'GetPlanPhases': grpc.unary_unary_rpc_method_handler( - servicer.GetPlanPhases, - request_deserializer=bionode__comm__pb2.SomniGetPlanPhasesReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetPlanPhasesRes.SerializeToString, - ), - 'GetInterventionEvents': grpc.unary_unary_rpc_method_handler( - servicer.GetInterventionEvents, - request_deserializer=bionode__comm__pb2.SomniGetInterventionEventsReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetInterventionEventsRes.SerializeToString, - ), - 'GetMonitorReportData': grpc.unary_unary_rpc_method_handler( - servicer.GetMonitorReportData, - request_deserializer=bionode__comm__pb2.SomniGetMonitorReportDataReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetMonitorReportDataRes.SerializeToString, - ), - 'GenerateMonitorDphReports': grpc.unary_unary_rpc_method_handler( - servicer.GenerateMonitorDphReports, - request_deserializer=bionode__comm__pb2.SomniGenerateMonitorDphReportsReq.FromString, - response_serializer=bionode__comm__pb2.SomniGenerateMonitorDphReportsRes.SerializeToString, - ), - 'GetSessionContext': grpc.unary_unary_rpc_method_handler( - servicer.GetSessionContext, - request_deserializer=bionode__comm__pb2.SomniGetSessionContextReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetSessionContextRes.SerializeToString, - ), - 'ConsoleStopDeviceSession': grpc.unary_unary_rpc_method_handler( - servicer.ConsoleStopDeviceSession, - request_deserializer=bionode__comm__pb2.SomniConsoleStopDeviceSessionReq.FromString, - response_serializer=bionode__comm__pb2.SomniConsoleStopDeviceSessionRes.SerializeToString, - ), - 'TriggerIntervention': grpc.unary_unary_rpc_method_handler( - servicer.TriggerIntervention, - request_deserializer=bionode__comm__pb2.TriggerInterventionReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'StopIntervention': grpc.unary_unary_rpc_method_handler( - servicer.StopIntervention, - request_deserializer=bionode__comm__pb2.StopInterventionReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'ResetAiThoughtRehearsalState': grpc.unary_unary_rpc_method_handler( - servicer.ResetAiThoughtRehearsalState, - request_deserializer=bionode__comm__pb2.ResetAiThoughtRehearsalStateReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'EmitDeviceLinkAiThought': grpc.unary_unary_rpc_method_handler( - servicer.EmitDeviceLinkAiThought, - request_deserializer=bionode__comm__pb2.EmitDeviceLinkAiThoughtReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'GetChatMessages': grpc.unary_unary_rpc_method_handler( - servicer.GetChatMessages, - request_deserializer=bionode__comm__pb2.GetChatMessagesReq.FromString, - response_serializer=bionode__comm__pb2.ChatMessageListRes.SerializeToString, - ), - 'ListChatConversations': grpc.unary_unary_rpc_method_handler( - servicer.ListChatConversations, - request_deserializer=bionode__comm__pb2.ListChatConversationsReq.FromString, - response_serializer=bionode__comm__pb2.ChatConversationListRes.SerializeToString, - ), - 'ListUserChatConversations': grpc.unary_unary_rpc_method_handler( - servicer.ListUserChatConversations, - request_deserializer=bionode__comm__pb2.ListUserChatConversationsReq.FromString, - response_serializer=bionode__comm__pb2.ChatConversationListRes.SerializeToString, - ), - 'GetTempPlan': grpc.unary_unary_rpc_method_handler( - servicer.GetTempPlan, - request_deserializer=bionode__comm__pb2.SomniGetTempPlanReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetTempPlanRes.SerializeToString, - ), - 'UploadChatAudio': grpc.unary_unary_rpc_method_handler( - servicer.UploadChatAudio, - request_deserializer=bionode__comm__pb2.UploadChatAudioReq.FromString, - response_serializer=bionode__comm__pb2.UploadChatAudioRes.SerializeToString, - ), - 'AppendChatMessage': grpc.unary_unary_rpc_method_handler( - servicer.AppendChatMessage, - request_deserializer=bionode__comm__pb2.AppendChatMessageReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'GetSleepMapCityOverview': grpc.unary_unary_rpc_method_handler( - servicer.GetSleepMapCityOverview, - request_deserializer=bionode__comm__pb2.SomniGetSleepMapCityOverviewReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetSleepMapCityOverviewRes.SerializeToString, - ), - 'GetSleepMapHighlights': grpc.unary_unary_rpc_method_handler( - servicer.GetSleepMapHighlights, - request_deserializer=bionode__comm__pb2.SomniGetSleepMapHighlightsReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetSleepMapHighlightsRes.SerializeToString, - ), - 'GetSleepMapCityRanking': grpc.unary_unary_rpc_method_handler( - servicer.GetSleepMapCityRanking, - request_deserializer=bionode__comm__pb2.SomniGetSleepMapCityRankingReq.FromString, - response_serializer=bionode__comm__pb2.SomniGetSleepMapCityRankingRes.SerializeToString, - ), - 'CreateDphConsoleSession': grpc.unary_unary_rpc_method_handler( - servicer.CreateDphConsoleSession, - request_deserializer=bionode__comm__pb2.CreateDphConsoleSessionReq.FromString, - response_serializer=bionode__comm__pb2.CreateDphConsoleSessionRes.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'bionode.comm.v1.SomniService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('bionode.comm.v1.SomniService', rpc_method_handlers) - - - # This class is part of an EXPERIMENTAL API. -class SomniService: - """==================== SomniService ==================== - 睡眠报告、方案、基线、融合数据 - 对应表: somni_*(报告、会话、方案、事件、生理、环境等) - - """ - - @staticmethod - def SetAlarm(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/SetAlarm', - bionode__comm__pb2.SomniSetAlarmReq.SerializeToString, - bionode__comm__pb2.SomniSetAlarmRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetPlan(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetPlan', - bionode__comm__pb2.SomniGetPlanReq.SerializeToString, - bionode__comm__pb2.SomniGetPlanRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def MarkPlanStarted(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/MarkPlanStarted', - bionode__comm__pb2.SomniMarkPlanStartedReq.SerializeToString, - bionode__comm__pb2.SomniMarkPlanStartedRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def MarkPlanStopped(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/MarkPlanStopped', - bionode__comm__pb2.SomniMarkPlanStoppedReq.SerializeToString, - bionode__comm__pb2.SomniMarkPlanStoppedRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def StartSomniDeviceSession(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/StartSomniDeviceSession', - bionode__comm__pb2.SomniStartSomniDeviceSessionReq.SerializeToString, - bionode__comm__pb2.SomniStartSomniDeviceSessionRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def StopSomniDeviceSession(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/StopSomniDeviceSession', - bionode__comm__pb2.SomniStopSomniDeviceSessionReq.SerializeToString, - bionode__comm__pb2.SomniStopSomniDeviceSessionRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetFusion(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetFusion', - bionode__comm__pb2.SomniGetFusionReq.SerializeToString, - bionode__comm__pb2.SomniGetFusionRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetBaseline(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetBaseline', - bionode__comm__pb2.SomniGetBaselineReq.SerializeToString, - bionode__comm__pb2.SomniGetBaselineRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetReport(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetReport', - bionode__comm__pb2.SomniGetReportReq.SerializeToString, - bionode__comm__pb2.SomniGetReportRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetSleepPlanet(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetSleepPlanet', - bionode__comm__pb2.SomniGetSleepPlanetReq.SerializeToString, - bionode__comm__pb2.SomniGetSleepPlanetRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListSleepPlanets(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/ListSleepPlanets', - bionode__comm__pb2.SomniListSleepPlanetsReq.SerializeToString, - bionode__comm__pb2.SomniListSleepPlanetsRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ValidateMonitorSession(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/ValidateMonitorSession', - bionode__comm__pb2.SomniValidateMonitorSessionReq.SerializeToString, - bionode__comm__pb2.SomniValidateMonitorSessionRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetMonitorPayload(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetMonitorPayload', - bionode__comm__pb2.SomniGetMonitorPayloadReq.SerializeToString, - bionode__comm__pb2.SomniGetMonitorPayloadRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetUidByMhrCodes(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetUidByMhrCodes', - bionode__comm__pb2.SomniGetUidByMhrCodesReq.SerializeToString, - bionode__comm__pb2.SomniGetUidByMhrCodesRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def CreateQuizSession(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/CreateQuizSession', - bionode__comm__pb2.SomniCreateQuizSessionReq.SerializeToString, - bionode__comm__pb2.SomniCreateQuizSessionRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpsertSomniAppInstUser(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/UpsertSomniAppInstUser', - bionode__comm__pb2.SomniUpsertSomniAppInstUserReq.SerializeToString, - bionode__comm__pb2.SomniUpsertSomniAppInstUserRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def DeleteSomniAppInstUser(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/DeleteSomniAppInstUser', - bionode__comm__pb2.SomniDeleteSomniAppInstUserReq.SerializeToString, - bionode__comm__pb2.SomniUpsertSomniAppInstUserRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def SomniQuizChat(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/SomniQuizChat', - bionode__comm__pb2.SomniQuizChatReq.SerializeToString, - bionode__comm__pb2.SomniQuizChatRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListInterventionConfigs(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/ListInterventionConfigs', - bionode__comm__pb2.ListInterventionConfigsReq.SerializeToString, - bionode__comm__pb2.InterventionConfigListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetInterventionConfig(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetInterventionConfig', - bionode__comm__pb2.IdReq.SerializeToString, - bionode__comm__pb2.InterventionConfigRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpsertInterventionConfig(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/UpsertInterventionConfig', - bionode__comm__pb2.UpsertInterventionConfigReq.SerializeToString, - bionode__comm__pb2.InterventionConfigRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def DeleteInterventionConfig(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/DeleteInterventionConfig', - bionode__comm__pb2.IdReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetPlanPhases(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetPlanPhases', - bionode__comm__pb2.SomniGetPlanPhasesReq.SerializeToString, - bionode__comm__pb2.SomniGetPlanPhasesRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetInterventionEvents(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetInterventionEvents', - bionode__comm__pb2.SomniGetInterventionEventsReq.SerializeToString, - bionode__comm__pb2.SomniGetInterventionEventsRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetMonitorReportData(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetMonitorReportData', - bionode__comm__pb2.SomniGetMonitorReportDataReq.SerializeToString, - bionode__comm__pb2.SomniGetMonitorReportDataRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GenerateMonitorDphReports(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GenerateMonitorDphReports', - bionode__comm__pb2.SomniGenerateMonitorDphReportsReq.SerializeToString, - bionode__comm__pb2.SomniGenerateMonitorDphReportsRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetSessionContext(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetSessionContext', - bionode__comm__pb2.SomniGetSessionContextReq.SerializeToString, - bionode__comm__pb2.SomniGetSessionContextRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ConsoleStopDeviceSession(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/ConsoleStopDeviceSession', - bionode__comm__pb2.SomniConsoleStopDeviceSessionReq.SerializeToString, - bionode__comm__pb2.SomniConsoleStopDeviceSessionRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def TriggerIntervention(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/TriggerIntervention', - bionode__comm__pb2.TriggerInterventionReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def StopIntervention(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/StopIntervention', - bionode__comm__pb2.StopInterventionReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ResetAiThoughtRehearsalState(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/ResetAiThoughtRehearsalState', - bionode__comm__pb2.ResetAiThoughtRehearsalStateReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def EmitDeviceLinkAiThought(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/EmitDeviceLinkAiThought', - bionode__comm__pb2.EmitDeviceLinkAiThoughtReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetChatMessages(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetChatMessages', - bionode__comm__pb2.GetChatMessagesReq.SerializeToString, - bionode__comm__pb2.ChatMessageListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListChatConversations(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/ListChatConversations', - bionode__comm__pb2.ListChatConversationsReq.SerializeToString, - bionode__comm__pb2.ChatConversationListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListUserChatConversations(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/ListUserChatConversations', - bionode__comm__pb2.ListUserChatConversationsReq.SerializeToString, - bionode__comm__pb2.ChatConversationListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetTempPlan(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetTempPlan', - bionode__comm__pb2.SomniGetTempPlanReq.SerializeToString, - bionode__comm__pb2.SomniGetTempPlanRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UploadChatAudio(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/UploadChatAudio', - bionode__comm__pb2.UploadChatAudioReq.SerializeToString, - bionode__comm__pb2.UploadChatAudioRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def AppendChatMessage(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/AppendChatMessage', - bionode__comm__pb2.AppendChatMessageReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetSleepMapCityOverview(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetSleepMapCityOverview', - bionode__comm__pb2.SomniGetSleepMapCityOverviewReq.SerializeToString, - bionode__comm__pb2.SomniGetSleepMapCityOverviewRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetSleepMapHighlights(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetSleepMapHighlights', - bionode__comm__pb2.SomniGetSleepMapHighlightsReq.SerializeToString, - bionode__comm__pb2.SomniGetSleepMapHighlightsRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetSleepMapCityRanking(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/GetSleepMapCityRanking', - bionode__comm__pb2.SomniGetSleepMapCityRankingReq.SerializeToString, - bionode__comm__pb2.SomniGetSleepMapCityRankingRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def CreateDphConsoleSession(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.SomniService/CreateDphConsoleSession', - bionode__comm__pb2.CreateDphConsoleSessionReq.SerializeToString, - bionode__comm__pb2.CreateDphConsoleSessionRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - -class AudioMaterialServiceStub: - """==================== AudioMaterialService ==================== - 音频原料 & 分类/标签管理 - 原料表: somni_audio_materials(Create/Update/Delete/Get/ListAudioMaterial*) - 分类/标签字典表: somni_audio_tag_dictionary(List/Get/Create/Update/DeleteAudioMaterialCategory*) - (H5 enrich 仍可能内部读取旧表 audio_materials,不暴露 Legacy RPC) - - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.ListAudioMaterialCategories = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/ListAudioMaterialCategories', - request_serializer=bionode__comm__pb2.ListAudioMaterialCategoriesReq.SerializeToString, - response_deserializer=bionode__comm__pb2.AudioMaterialCategoryListRes.FromString, - _registered_method=True) - self.GetAudioMaterialCategory = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/GetAudioMaterialCategory', - request_serializer=bionode__comm__pb2.IdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.AudioMaterialCategoryRes.FromString, - _registered_method=True) - self.CreateAudioMaterialCategory = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/CreateAudioMaterialCategory', - request_serializer=bionode__comm__pb2.CreateAudioMaterialCategoryReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.UpdateAudioMaterialCategory = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/UpdateAudioMaterialCategory', - request_serializer=bionode__comm__pb2.UpdateAudioMaterialCategoryReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.DeleteAudioMaterialCategory = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/DeleteAudioMaterialCategory', - request_serializer=bionode__comm__pb2.IdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.ListAudioMaterials = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/ListAudioMaterials', - request_serializer=bionode__comm__pb2.ListAudioMaterialsReq.SerializeToString, - response_deserializer=bionode__comm__pb2.AudioMaterialListRes.FromString, - _registered_method=True) - self.GetAudioMaterial = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/GetAudioMaterial', - request_serializer=bionode__comm__pb2.IdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.AudioMaterialRes.FromString, - _registered_method=True) - self.GetDistinctTags = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/GetDistinctTags', - request_serializer=bionode__comm__pb2.EmptyReq.SerializeToString, - response_deserializer=bionode__comm__pb2.DistinctTagsRes.FromString, - _registered_method=True) - self.CreateAudioMaterial = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/CreateAudioMaterial', - request_serializer=bionode__comm__pb2.CreateAudioMaterialReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.UpdateAudioMaterial = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/UpdateAudioMaterial', - request_serializer=bionode__comm__pb2.UpdateAudioMaterialReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.UpdateAudioMaterialStatus = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/UpdateAudioMaterialStatus', - request_serializer=bionode__comm__pb2.UpdateAudioMaterialStatusReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - self.DeleteAudioMaterial = channel.unary_unary( - '/bionode.comm.v1.AudioMaterialService/DeleteAudioMaterial', - request_serializer=bionode__comm__pb2.IdReq.SerializeToString, - response_deserializer=bionode__comm__pb2.EmptyRes.FromString, - _registered_method=True) - - -class AudioMaterialServiceServicer: - """==================== AudioMaterialService ==================== - 音频原料 & 分类/标签管理 - 原料表: somni_audio_materials(Create/Update/Delete/Get/ListAudioMaterial*) - 分类/标签字典表: somni_audio_tag_dictionary(List/Get/Create/Update/DeleteAudioMaterialCategory*) - (H5 enrich 仍可能内部读取旧表 audio_materials,不暴露 Legacy RPC) - - """ - - def ListAudioMaterialCategories(self, request, context): - """----- 分类/标签字典 somni_audio_tag_dictionary ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetAudioMaterialCategory(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def CreateAudioMaterialCategory(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpdateAudioMaterialCategory(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def DeleteAudioMaterialCategory(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListAudioMaterials(self, request, context): - """----- 原料 somni_audio_materials ----- - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetAudioMaterial(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetDistinctTags(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def CreateAudioMaterial(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpdateAudioMaterial(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def UpdateAudioMaterialStatus(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def DeleteAudioMaterial(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_AudioMaterialServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'ListAudioMaterialCategories': grpc.unary_unary_rpc_method_handler( - servicer.ListAudioMaterialCategories, - request_deserializer=bionode__comm__pb2.ListAudioMaterialCategoriesReq.FromString, - response_serializer=bionode__comm__pb2.AudioMaterialCategoryListRes.SerializeToString, - ), - 'GetAudioMaterialCategory': grpc.unary_unary_rpc_method_handler( - servicer.GetAudioMaterialCategory, - request_deserializer=bionode__comm__pb2.IdReq.FromString, - response_serializer=bionode__comm__pb2.AudioMaterialCategoryRes.SerializeToString, - ), - 'CreateAudioMaterialCategory': grpc.unary_unary_rpc_method_handler( - servicer.CreateAudioMaterialCategory, - request_deserializer=bionode__comm__pb2.CreateAudioMaterialCategoryReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'UpdateAudioMaterialCategory': grpc.unary_unary_rpc_method_handler( - servicer.UpdateAudioMaterialCategory, - request_deserializer=bionode__comm__pb2.UpdateAudioMaterialCategoryReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'DeleteAudioMaterialCategory': grpc.unary_unary_rpc_method_handler( - servicer.DeleteAudioMaterialCategory, - request_deserializer=bionode__comm__pb2.IdReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'ListAudioMaterials': grpc.unary_unary_rpc_method_handler( - servicer.ListAudioMaterials, - request_deserializer=bionode__comm__pb2.ListAudioMaterialsReq.FromString, - response_serializer=bionode__comm__pb2.AudioMaterialListRes.SerializeToString, - ), - 'GetAudioMaterial': grpc.unary_unary_rpc_method_handler( - servicer.GetAudioMaterial, - request_deserializer=bionode__comm__pb2.IdReq.FromString, - response_serializer=bionode__comm__pb2.AudioMaterialRes.SerializeToString, - ), - 'GetDistinctTags': grpc.unary_unary_rpc_method_handler( - servicer.GetDistinctTags, - request_deserializer=bionode__comm__pb2.EmptyReq.FromString, - response_serializer=bionode__comm__pb2.DistinctTagsRes.SerializeToString, - ), - 'CreateAudioMaterial': grpc.unary_unary_rpc_method_handler( - servicer.CreateAudioMaterial, - request_deserializer=bionode__comm__pb2.CreateAudioMaterialReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'UpdateAudioMaterial': grpc.unary_unary_rpc_method_handler( - servicer.UpdateAudioMaterial, - request_deserializer=bionode__comm__pb2.UpdateAudioMaterialReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'UpdateAudioMaterialStatus': grpc.unary_unary_rpc_method_handler( - servicer.UpdateAudioMaterialStatus, - request_deserializer=bionode__comm__pb2.UpdateAudioMaterialStatusReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - 'DeleteAudioMaterial': grpc.unary_unary_rpc_method_handler( - servicer.DeleteAudioMaterial, - request_deserializer=bionode__comm__pb2.IdReq.FromString, - response_serializer=bionode__comm__pb2.EmptyRes.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'bionode.comm.v1.AudioMaterialService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('bionode.comm.v1.AudioMaterialService', rpc_method_handlers) - - - # This class is part of an EXPERIMENTAL API. -class AudioMaterialService: - """==================== AudioMaterialService ==================== - 音频原料 & 分类/标签管理 - 原料表: somni_audio_materials(Create/Update/Delete/Get/ListAudioMaterial*) - 分类/标签字典表: somni_audio_tag_dictionary(List/Get/Create/Update/DeleteAudioMaterialCategory*) - (H5 enrich 仍可能内部读取旧表 audio_materials,不暴露 Legacy RPC) - - """ - - @staticmethod - def ListAudioMaterialCategories(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/ListAudioMaterialCategories', - bionode__comm__pb2.ListAudioMaterialCategoriesReq.SerializeToString, - bionode__comm__pb2.AudioMaterialCategoryListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetAudioMaterialCategory(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/GetAudioMaterialCategory', - bionode__comm__pb2.IdReq.SerializeToString, - bionode__comm__pb2.AudioMaterialCategoryRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def CreateAudioMaterialCategory(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/CreateAudioMaterialCategory', - bionode__comm__pb2.CreateAudioMaterialCategoryReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpdateAudioMaterialCategory(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/UpdateAudioMaterialCategory', - bionode__comm__pb2.UpdateAudioMaterialCategoryReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def DeleteAudioMaterialCategory(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/DeleteAudioMaterialCategory', - bionode__comm__pb2.IdReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def ListAudioMaterials(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/ListAudioMaterials', - bionode__comm__pb2.ListAudioMaterialsReq.SerializeToString, - bionode__comm__pb2.AudioMaterialListRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetAudioMaterial(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/GetAudioMaterial', - bionode__comm__pb2.IdReq.SerializeToString, - bionode__comm__pb2.AudioMaterialRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetDistinctTags(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/GetDistinctTags', - bionode__comm__pb2.EmptyReq.SerializeToString, - bionode__comm__pb2.DistinctTagsRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def CreateAudioMaterial(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/CreateAudioMaterial', - bionode__comm__pb2.CreateAudioMaterialReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpdateAudioMaterial(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/UpdateAudioMaterial', - bionode__comm__pb2.UpdateAudioMaterialReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def UpdateAudioMaterialStatus(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/UpdateAudioMaterialStatus', - bionode__comm__pb2.UpdateAudioMaterialStatusReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def DeleteAudioMaterial(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/bionode.comm.v1.AudioMaterialService/DeleteAudioMaterial', - bionode__comm__pb2.IdReq.SerializeToString, - bionode__comm__pb2.EmptyRes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) diff --git a/app/bionode_grpc_clients/comm/grpc_gen/bionode_common_pb2.py b/app/bionode_grpc_clients/comm/grpc_gen/bionode_common_pb2.py deleted file mode 100644 index 41a116e..0000000 --- a/app/bionode_grpc_clients/comm/grpc_gen/bionode_common_pb2.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: bionode_common.proto -# Protobuf Python Version: 6.33.5 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 33, - 5, - '', - 'bionode_common.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x62ionode_common.proto\x12\x11\x62ionode.common.v1\"\x07\n\x05\x45mpty\"\x17\n\tIdRequest\x12\n\n\x02id\x18\x01 \x01(\t\"\x19\n\nIdsRequest\x12\x0b\n\x03ids\x18\x01 \x03(\t\"Q\n\x0bPageRequest\x12\x0c\n\x04page\x18\x01 \x01(\x05\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x0f\n\x07keyword\x18\x03 \x01(\t\x12\x10\n\x08order_by\x18\x04 \x01(\t\"S\n\x0cPageResponse\x12\r\n\x05total\x18\x01 \x01(\x05\x12\x0c\n\x04page\x18\x02 \x01(\x05\x12\x11\n\tpage_size\x18\x03 \x01(\x05\x12\x13\n\x0btotal_pages\x18\x04 \x01(\x05\"D\n\x08Metadata\x12\x0e\n\x06status\x18\x01 \x01(\x05\x12\x13\n\x0b\x63reate_time\x18\x02 \x01(\t\x12\x13\n\x0bupdate_time\x18\x03 \x01(\t\",\n\x11OperationResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0b\n\x03msg\x18\x02 \x01(\t\"[\n\nOptionItem\x12\r\n\x05value\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\x12/\n\x08\x63hildren\x18\x03 \x03(\x0b\x32\x1d.bionode.common.v1.OptionItem\"<\n\x05\x45rror\x12\x0c\n\x04\x63ode\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x14\n\x0c\x64\x65tails_json\x18\x03 \x01(\tb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'bionode_common_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_EMPTY']._serialized_start=43 - _globals['_EMPTY']._serialized_end=50 - _globals['_IDREQUEST']._serialized_start=52 - _globals['_IDREQUEST']._serialized_end=75 - _globals['_IDSREQUEST']._serialized_start=77 - _globals['_IDSREQUEST']._serialized_end=102 - _globals['_PAGEREQUEST']._serialized_start=104 - _globals['_PAGEREQUEST']._serialized_end=185 - _globals['_PAGERESPONSE']._serialized_start=187 - _globals['_PAGERESPONSE']._serialized_end=270 - _globals['_METADATA']._serialized_start=272 - _globals['_METADATA']._serialized_end=340 - _globals['_OPERATIONRESPONSE']._serialized_start=342 - _globals['_OPERATIONRESPONSE']._serialized_end=386 - _globals['_OPTIONITEM']._serialized_start=388 - _globals['_OPTIONITEM']._serialized_end=479 - _globals['_ERROR']._serialized_start=481 - _globals['_ERROR']._serialized_end=541 -# @@protoc_insertion_point(module_scope) diff --git a/app/bionode_grpc_clients/comm/grpc_gen/bionode_common_pb2_grpc.py b/app/bionode_grpc_clients/comm/grpc_gen/bionode_common_pb2_grpc.py deleted file mode 100644 index 613ce9e..0000000 --- a/app/bionode_grpc_clients/comm/grpc_gen/bionode_common_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.81.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in bionode_common_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/app/cache/sleep_stage_cache.py b/app/cache/sleep_stage_cache.py index 9b2954e..e1df956 100644 --- a/app/cache/sleep_stage_cache.py +++ b/app/cache/sleep_stage_cache.py @@ -6,6 +6,7 @@ from __future__ import annotations +import asyncio import hashlib import json from collections.abc import Awaitable, Callable @@ -15,7 +16,8 @@ from app.core.config import Settings -SLEEP_STAGES = ("放松", "入睡", "守护", "清醒") +SLEEP_STAGES = ("放松", "入睡", "守护", "唤醒") +_CACHE_CLEANUP_STAGES = (*SLEEP_STAGES, "清醒") SLEEP_STAGE_INDEX_KEY_PREFIX = "sleep_stage_v2_index:" SLEEP_STAGE_DOC_KEY_PREFIX = "sleep_stage_v2_doc:" # 兼容旧测试/调用名 @@ -92,6 +94,7 @@ def __init__(self, redis: RedisLike, *, ttl_sec: int = _WEEK_SECONDS) -> None: raise ValueError("ttl_sec must be >= 1") self._redis = redis self._ttl_sec = ttl_sec + self._mutation_lock = asyncio.Lock() async def get(self, stages: list[str]) -> list[dict[str, Any]] | None: """全部阶段索引命中且文档齐全才返回;否则 None。""" @@ -158,18 +161,50 @@ async def set_stage(self, stage: str, docs: list[dict[str, Any]]) -> None: len(urls), ) + async def get_or_load( + self, + stages: list[str], + loader: StageLoader, + ) -> list[dict[str, Any]]: + """读取缓存;未命中时仅加载缺失阶段,并合并同进程并发 miss。""" + normalized = _normalize_stages(stages) + cached = await self.get(normalized) + if cached is not None: + return cached + + async with self._mutation_lock: + cached = await self.get(normalized) + if cached is not None: + return cached + + for stage in normalized: + if await self.get([stage]) is not None: + continue + docs = await loader(stage) + await self.set_stage(stage, docs) + + loaded = await self.get(normalized) + if loaded is None: + raise RuntimeError("sleep stage cache load completed without a readable value") + return loaded + async def warm(self, loader: StageLoader) -> None: """清空后按四个阶段从数据源重建索引与文档。""" - await self.clear_all() - for stage in SLEEP_STAGES: - docs = await loader(stage) - await self.set_stage(stage, docs) + async with self._mutation_lock: + await self._clear_all_unlocked() + for stage in SLEEP_STAGES: + docs = await loader(stage) + await self.set_stage(stage, docs) logger.info("睡眠阶段候选缓存预热完成,stages={}", list(SLEEP_STAGES)) async def clear_all(self) -> None: """删除四个阶段索引及其引用的全部文档。""" + async with self._mutation_lock: + await self._clear_all_unlocked() + + async def _clear_all_unlocked(self) -> None: urls: set[str] = set() - index_keys = [build_sleep_stage_index_key(stage) for stage in SLEEP_STAGES] + index_keys = [build_sleep_stage_index_key(stage) for stage in _CACHE_CLEANUP_STAGES] for key in index_keys: raw = await self._redis.get(key) if raw is None: @@ -180,7 +215,7 @@ async def clear_all(self) -> None: legacy_urls: set[str] = set() legacy_index_keys = [ - f"{_LEGACY_SLEEP_STAGE_INDEX_KEY_PREFIX}{stage}" for stage in SLEEP_STAGES + f"{_LEGACY_SLEEP_STAGE_INDEX_KEY_PREFIX}{stage}" for stage in _CACHE_CLEANUP_STAGES ] for key in legacy_index_keys: raw = await self._redis.get(key) @@ -191,11 +226,9 @@ async def clear_all(self) -> None: legacy_urls.update(str(item) for item in items if item) doc_keys = [build_sleep_stage_doc_key(url) for url in urls] - legacy_doc_keys = [ - _build_legacy_sleep_stage_doc_key(url) for url in legacy_urls - ] + legacy_doc_keys = [_build_legacy_sleep_stage_doc_key(url) for url in legacy_urls] legacy_candidate_keys = [ - f"{_LEGACY_SLEEP_STAGE_CANDIDATE_KEY_PREFIX}{stage}" for stage in SLEEP_STAGES + f"{_LEGACY_SLEEP_STAGE_CANDIDATE_KEY_PREFIX}{stage}" for stage in _CACHE_CLEANUP_STAGES ] to_delete = [ *index_keys, diff --git a/app/core/bson_util.py b/app/core/bson_util.py new file mode 100644 index 0000000..a8ce67a --- /dev/null +++ b/app/core/bson_util.py @@ -0,0 +1,38 @@ +"""BSON / 时间小工具(供手板、量产、同步脚本复用)。""" + +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any + +from bson import ObjectId +from bson.errors import InvalidId + +from app.core.exceptions import MaterialNotFoundError + + +def utc_now() -> datetime: + return datetime.now(UTC) + + +def utc_now_iso() -> str: + return utc_now().isoformat().replace("+00:00", "Z") + + +def bson_to_jsonable(value: Any) -> Any: + if isinstance(value, ObjectId): + return str(value) + if isinstance(value, datetime): + return value.astimezone(UTC).isoformat().replace("+00:00", "Z") + if isinstance(value, dict): + return {k: bson_to_jsonable(v) for k, v in value.items()} + if isinstance(value, list): + return [bson_to_jsonable(v) for v in value] + return value + + +def parse_object_id(material_id: str) -> ObjectId: + try: + return ObjectId(material_id) + except InvalidId as exc: + raise MaterialNotFoundError(material_id) from exc diff --git a/app/core/config.py b/app/core/config.py index 1d20306..75b3210 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -20,6 +20,15 @@ class Settings(BaseSettings): app_port: int = 8080 app_debug: bool = False + # 功能手板 gRPC(与 HTTP 同进程;false 时跳过该端口) + grpc_host: str = "0.0.0.0" + grpc_port: int = 50065 + grpc_enabled: bool = True + + # 量产 gRPC + somni_grpc_port: int = 50064 + somni_grpc_enabled: bool = True + es_node: str = "http://localhost:9200" es_audio_index: str = "somni_audio_materials" es_tag_vectors_index: str = "somni_audio_tag_dictionary" @@ -27,16 +36,32 @@ class Settings(BaseSettings): es_request_timeout_sec: float = 30.0 search_max_concurrency: int = 25 # 同时执行的检索流水线上限,防止打满 ES + # 量产 ES(与手板向量/节点隔离) + somni_es_node: str = "" + somni_es_audio_index: str = "somni_audio_materials" + somni_es_tag_vectors_index: str = "somni_audio_tag_dictionary" + somni_es_search_events_index: str = "somni_audio_search_events" + mongo_uri: str = "" mongo_db: str = "Fullive" mongo_materials_collection: str = "somni_audio_materials" mongo_tag_dictionary_collection: str = "somni_audio_tag_dictionary" - comm_grpc_host: str = "bionode-test.fulai.tech" - comm_grpc_port: int = 443 - comm_grpc_use_tls: bool = True # 443 走 TLS;内网明文可设 false + # 量产 Mongo + somni_mongo_uri: str = "" + somni_mongo_db: str = "Somni" + somni_mongo_materials_collection: str = "somni_audio_materials" + somni_mongo_tag_dictionary_collection: str = "somni_audio_tag_dictionary" + somni_mongo_answers_collection: str = "somni_quiz_answers" + somni_mongo_devices_collection: str = "somni_devices" + somni_mongo_telemetry_collection: str = "somni_telemetry" + somni_mongo_records_collection: str = "somni_records" + somni_mongo_sleep_reports_collection: str = "somni_sleep_reports" + somni_mongo_events_collection: str = "somni_events" sim_threshold: float = 0.7 # 内容形态向量模糊命中阈值(规范 §五-2) + # GetAudio query_text 与内容形态标签(含二级)向量相似度下限 + get_audio_root_tag_sim_threshold: float = 0.75 # 多路文本检索厌恶硬剔除阈值;≥ 该值 penalty=1.0 丢弃候选 strong_dislike_sim_threshold: float = 0.85 search_sleep_stage_filter_enabled: bool = True # 检索步骤 1 是否按睡眠阶段过滤 @@ -56,7 +81,8 @@ class Settings(BaseSettings): log_level: str = "INFO" log_dir: str = "logs" - # 按日滚动:文件名为 YYYY-MM-DD_ubur_log,见 app.core.logging + # 按日命名 YYYY-MM-DD_ubur_log;超大小则同日再开新文件,见 app.core.logging + log_rotation_size: str = "100 MB" log_retention: str = "7 days" # Mongo → ES 差异同步(服务内定时 + scripts/sync_es_from_comm.py 手动) @@ -67,15 +93,28 @@ class Settings(BaseSettings): sync_backup_filename: str = "somni_audio_materials_backup.json" sync_tag_dictionary_backup_filename: str = "somni_audio_tag_dictionary_backup.json" - # 音频检索 Redis 缓存(空 URL 表示关闭) + # 功能手板 Redis(空 URL 表示关闭) redis_url: str = "" + # 量产 Redis(独立实例;空则 GetHot 关闭,不回退 redis_url) + somni_redis_url: str = "" + somni_hot_enabled: bool = True + somni_hot_top_n: int = 10 + somni_hot_redis_key: str = "somni:audio:hot:v1" + somni_redis_max_connections: int = 128 + somni_redis_connect_timeout_sec: float = 2.0 + somni_redis_socket_timeout_sec: float = 2.0 # 连接池需覆盖 HTTP 并发峰值;redis-py 默认仅 100,高并发易 Too many connections redis_max_connections: int = 512 search_cache_max_size: int = 2048 search_cache_ttl_sec: int = 604800 # 7 天 + somni_audio_catalog_cache_ttl_sec: float = 60.0 # CUD 后延时重建睡眠阶段候选缓存,窗口内多次写入只重建一次 sleep_stage_cache_rewarm_delay_sec: float = 5.0 + default_page_size: int = 20 + max_page_size: int = 200 + fetch_all_hard_limit: int = 5000 + @property def embedding_onnx_path(self) -> Path: return Path(self.embedding_onnx_dir) / "model.onnx" @@ -92,14 +131,15 @@ def sync_backup_path(self) -> Path: def sync_tag_dictionary_backup_path(self) -> Path: return Path(self.sync_backup_dir) / self.sync_tag_dictionary_backup_filename - @property - def comm_grpc_target(self) -> str: - return f"{self.comm_grpc_host}:{self.comm_grpc_port}" - @property def log_dir_path(self) -> Path: return Path(self.log_dir) + @property + def effective_somni_es_node(self) -> str: + """量产 ES 节点;未配置时回退手板(仅本地兜底,生产应显式配置)。""" + return self.somni_es_node or self.es_node + @lru_cache def get_settings() -> Settings: diff --git a/app/core/logging.py b/app/core/logging.py index 5c9ba80..9c6883b 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -1,15 +1,20 @@ -"""loguru 初始化:控制台(开发)+ 按日文件(生产采集)。 +"""loguru 初始化:控制台(开发)+ 按日/按大小文件(生产采集)。 -文件落在 LOG_DIR,每日一个,命名为 YYYY-MM-DD_ubur_log(不隐藏、不压缩)。 +文件落在 LOG_DIR,命名为 YYYY-MM-DD_ubur_log。 +- 本地日历跨日:切到新日期文件。 +- 单日文件超过 log_rotation_size:旧文件追加时间戳后缀归档,新建同名文件继续写。 enqueue=True:异步写盘,避免阻塞请求线程。 extra.request_id:并发请求可按 request_id 串联整条调用链日志。 """ from __future__ import annotations +import re import sys +from collections.abc import Callable from datetime import date from pathlib import Path +from typing import IO, Any from loguru import logger @@ -30,7 +35,9 @@ DEFAULT_REQUEST_ID = "-" LOG_FILE_SUFFIX = "_ubur_log" -_DAILY_ROTATION = "00:00" +_SIZE_PATTERN = re.compile( + r"([eE+\-\.\d]+)\s*([kmgtpezyKMGTPEZY])?(i)?([bB])" +) def current_log_path(log_dir: Path, when: date | None = None) -> Path: @@ -39,12 +46,40 @@ def current_log_path(log_dir: Path, when: date | None = None) -> Path: return log_dir / f"{day:%Y-%m-%d}{LOG_FILE_SUFFIX}" +def parse_log_size(size: str) -> int: + """将 '100 MB' / '1 GiB' 等解析为字节数(规则与 loguru 一致)。""" + match = _SIZE_PATTERN.fullmatch(size.strip()) + if match is None: + raise ValueError(f"无法解析 log_rotation_size: {size!r}") + number_text, unit, binary, byte_unit = match.groups() + number = float(number_text) + unit_power = "kmgtpezy".index(unit.lower()) + 1 if unit else 0 + base = 1024 if binary else 1000 + bit_div = 8 if byte_unit == "b" else 1 + return int(number * (base**unit_power) / bit_div) + + +def daily_or_size_rotation(max_bytes: int) -> Callable[[Any, IO[str]], bool]: + """跨日或超过大小时滚动;同日满文件由 loguru 重命名旧文件后再开新文件。""" + + def should_rotate(message: Any, file: IO[str]) -> bool: + file.seek(0, 2) + if file.tell() + len(message) > max_bytes: + return True + file_day = Path(file.name).name[:10] + msg_day = message.record["time"].strftime("%Y-%m-%d") + return file_day != msg_day + + return should_rotate + + def setup_logging(settings: Settings) -> Path: """初始化双输出;返回当天日志文件路径供启动日志引用。""" log_dir = settings.log_dir_path log_dir.mkdir(parents=True, exist_ok=True) log_file = current_log_path(log_dir) sink_pattern = str(log_dir / f"{{time:YYYY-MM-DD}}{LOG_FILE_SUFFIX}") + max_bytes = parse_log_size(settings.log_rotation_size) logger.remove() logger.configure(extra={"request_id": DEFAULT_REQUEST_ID}) @@ -54,10 +89,15 @@ def setup_logging(settings: Settings) -> Path: sink_pattern, level=level, format=FILE_LOG_FORMAT, - rotation=_DAILY_ROTATION, + rotation=daily_or_size_rotation(max_bytes), retention=settings.log_retention, encoding="utf-8", enqueue=True, ) - logger.info("日志已初始化,级别={},文件={}", level, log_file) + logger.info( + "日志已初始化,级别={},文件={},单文件上限={}", + level, + log_file, + settings.log_rotation_size, + ) return log_file diff --git a/app/core/somni_redis.py b/app/core/somni_redis.py new file mode 100644 index 0000000..7d3475e --- /dev/null +++ b/app/core/somni_redis.py @@ -0,0 +1,40 @@ +"""量产 Redis 客户端(与功能手板/HTTP Redis 物理隔离)。""" + +from __future__ import annotations + +from loguru import logger +from redis.asyncio import Redis + +from app.core.config import Settings + + +def resolve_somni_redis_url(settings: Settings) -> str: + return settings.somni_redis_url.strip() + + +async def create_somni_redis(settings: Settings) -> Redis | None: + """仅按 SOMNI_REDIS_URL 建连接;启用热点时配置错误立即阻止启动。""" + if not settings.somni_hot_enabled: + return None + url = resolve_somni_redis_url(settings) + if not url: + logger.warning("未配置 SOMNI_REDIS_URL,量产 GetHot 热点排行不可用") + return None + client = Redis.from_url( + url, + decode_responses=True, + max_connections=max(1, settings.somni_redis_max_connections), + socket_connect_timeout=max(0.1, settings.somni_redis_connect_timeout_sec), + socket_timeout=max(0.1, settings.somni_redis_socket_timeout_sec), + health_check_interval=30, + ) + try: + await client.ping() + except Exception: + await client.aclose() + raise + logger.info( + "已连接量产独立 Redis,max_connections={}", + settings.somni_redis_max_connections, + ) + return client diff --git a/app/es/client.py b/app/es/client.py index e031fcd..e3e45f6 100644 --- a/app/es/client.py +++ b/app/es/client.py @@ -7,10 +7,14 @@ from app.core.config import Settings -def create_es_client(settings: Settings) -> AsyncElasticsearch: +def create_es_client( + settings: Settings, + *, + node: str | None = None, +) -> AsyncElasticsearch: """创建进程级 ES 客户端;高并发检索需足够 connections_per_node。""" return AsyncElasticsearch( - settings.es_node, + node or settings.es_node, connections_per_node=settings.es_connections_per_node, request_timeout=settings.es_request_timeout_sec, retry_on_timeout=True, diff --git a/app/es/index_mappings.py b/app/es/index_mappings.py index 1838625..4e3bbbc 100644 --- a/app/es/index_mappings.py +++ b/app/es/index_mappings.py @@ -51,6 +51,7 @@ def build_somni_audio_materials_mapping(embedding_dim: int) -> dict[str, Any]: "description_vector": f"description_text 的 {embedding_dim} 维 embedding", "status": "是否启用;true 表示可参与检索与同步", "audio_url": "音频文件 CDN / 对象存储 URL", + "cover_url": "封面图 CDN / 对象存储 URL", "operation_type": "0 大模型打标,1 人工打标", "tag_id": "标签 ID,关联 somni_audio_tag_dictionary._id", "code": "标签编码,用于规则判断与接口传输", @@ -98,6 +99,7 @@ def build_somni_audio_materials_mapping(embedding_dim: int) -> dict[str, Any]: ), "status": _field("boolean", "是否启用;true 表示可参与检索与同步"), "audio_url": _field("keyword", "音频文件地址(CDN / 对象存储 / 内部资源 URL)"), + "cover_url": _field("keyword", "封面图地址(CDN / 对象存储 / 内部资源 URL)"), "operation_type": _field( "integer", "标注操作类型:0 大模型打标,1 人工打标", diff --git a/app/es/search.py b/app/es/search.py index cc967a8..a7a3129 100644 --- a/app/es/search.py +++ b/app/es/search.py @@ -26,6 +26,7 @@ "audio_name", "description", "audio_url", + "cover_url", "sleep_stage_tags", "content_form_tags", "mechanism_tags", @@ -96,9 +97,20 @@ def _candidate_search_body(query: dict[str, Any], *, size: int = 1000) -> dict[s class EsSearch: """封装检索相关的 ES 查询与文档解析。""" - def __init__(self, client: AsyncElasticsearch, settings: Settings) -> None: + def __init__( + self, + client: AsyncElasticsearch, + settings: Settings, + *, + audio_index: str | None = None, + tag_dictionary_index: str | None = None, + ) -> None: self._client = client self._settings = settings + self._audio_index = audio_index or settings.es_audio_index + self._tag_dictionary_index = ( + tag_dictionary_index or settings.es_tag_vectors_index + ) self._content_tag_vectors_cache: list[dict[str, Any]] | None = None self._content_tag_vectors_lock = asyncio.Lock() # 按 tag_id 缓存 name_vector,避免每请求 mget(内容准入模糊路径) @@ -107,11 +119,11 @@ def __init__(self, client: AsyncElasticsearch, settings: Settings) -> None: @property def audio_index(self) -> str: - return self._settings.es_audio_index + return self._audio_index @property def tag_dictionary_index(self) -> str: - return self._settings.es_tag_vectors_index + return self._tag_dictionary_index @property def tag_vectors_index(self) -> str: @@ -320,6 +332,7 @@ async def _fetch_content_tag_vectors(self, *, size: int) -> list[dict[str, Any]] "label": label, "dimension": source.get("type", ""), "vector": vector, + "parent_tag_id": str(source.get("parent_tag_id") or ""), } ) return tags @@ -379,6 +392,18 @@ def content_tag_ids(tags: AudioTags) -> list[str]: ids.extend(item.vector_id for item in dim) return ids + async def list_audio_catalog_docs(self, *, size: int) -> list[dict[str, Any]]: + """量产 GetAudio:音频全量(不含 embedding),供内存过滤。""" + response = await self._client.search( + index=self.audio_index, + body={ + "query": {"match_all": {}}, + "size": max(1, size), + "_source": {"excludes": ["embedding", "description_vector"]}, + }, + ) + return [_document_from_hit(hit) for hit in response["hits"]["hits"]] + async def migrate_legacy_indices(self) -> None: """删除旧版 audio_materials / tag_vectors 索引。""" for index in LEGACY_INDICES: diff --git a/app/es/search_events.py b/app/es/search_events.py new file mode 100644 index 0000000..4b03793 --- /dev/null +++ b/app/es/search_events.py @@ -0,0 +1,95 @@ +"""量产音频搜索事件 ES 明细。""" + +from __future__ import annotations + +import asyncio +from datetime import UTC, datetime +from typing import Any + +from loguru import logger + +from app.core.config import Settings + +_MAPPING = { + "mappings": { + "properties": { + "keyword": {"type": "keyword"}, + "raw_query": {"type": "keyword"}, + "created_at": {"type": "date"}, + "hit_count": {"type": "integer"}, + "request_id": {"type": "keyword"}, + } + } +} + + +def _build_event_doc( + *, + keyword: str, + raw_query: str, + hit_count: int, + request_id: str, +) -> dict[str, Any]: + return { + "keyword": keyword, + "raw_query": raw_query, + "created_at": datetime.now(UTC).isoformat(), + "hit_count": int(hit_count), + "request_id": request_id, + } + + +class SearchEventsStore: + def __init__(self, client: Any, settings: Settings) -> None: + self._client = client + self._index = settings.somni_es_search_events_index + self._ensure_lock = asyncio.Lock() + self._index_ready = False + + async def ensure_index(self) -> None: + if self._index_ready: + return + async with self._ensure_lock: + if self._index_ready: + return + if await self._client.indices.exists(index=self._index): + self._index_ready = True + return + try: + await self._client.indices.create(index=self._index, body=_MAPPING) + logger.info("已创建 ES 索引:{}", self._index) + except Exception as exc: + if not _is_already_exists_error(exc): + raise + self._index_ready = True + + async def index_event( + self, + *, + keyword: str, + raw_query: str, + hit_count: int, + request_id: str = "", + ) -> None: + await self.ensure_index() + doc = _build_event_doc( + keyword=keyword, + raw_query=raw_query, + hit_count=hit_count, + request_id=request_id, + ) + await self._client.index(index=self._index, document=doc) + + +def _is_already_exists_error(exc: Exception) -> bool: + details = ( + str(exc), + str(getattr(exc, "error", "")), + str(getattr(exc, "body", "")), + str(getattr(exc, "info", "")), + ) + return any( + marker in detail + for detail in details + for marker in ("resource_already_exists_exception", "index_already_exists_exception") + ) diff --git a/app/main.py b/app/main.py index 23ae285..e05b4f9 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,7 @@ """FastAPI 应用入口。 职责: -- lifespan 内单例预热 ES / Embedding / gRPC 客户端(规范要求勿每请求重建) +- lifespan 内预热 ES / Embedding / Mongo;启功能手板 + 量产 gRPC - 挂载 HTTP 路由与请求日志中间件 - 通过 AppState 向 API 层提供已初始化的服务实例 """ @@ -17,13 +17,13 @@ if TYPE_CHECKING: from elasticsearch import AsyncElasticsearch + from motor.motor_asyncio import AsyncIOMotorClient _PROJECT_ROOT = Path(__file__).resolve().parents[1] _VENV_PYTHON = _PROJECT_ROOT / ".venv" / "bin" / "python" def _bootstrap_dev_entry() -> None: - """直接运行 main.py 时切到项目 .venv,并保证可 import app 包。""" if __name__ != "__main__": return root = str(_PROJECT_ROOT) @@ -40,9 +40,9 @@ def _bootstrap_dev_entry() -> None: from fastapi import FastAPI from loguru import logger +from motor.motor_asyncio import AsyncIOMotorClient from app.api.audio import router as audio_router -from app.bionode_grpc_clients import CommClient from app.cache.audio_search_cache import ( AudioSearchCache, create_audio_search_cache, @@ -55,32 +55,42 @@ def _bootstrap_dev_entry() -> None: from app.core.config import Settings, get_settings from app.core.exception_handlers import register_exception_handlers from app.core.logging import setup_logging +from app.core.somni_redis import create_somni_redis from app.embedding.encoder import Encoder, create_encoder from app.es.client import create_es_client from app.es.search import EsSearch +from app.es.search_events import SearchEventsStore from app.es.sync import EsSync from app.middleware.request_log import register_request_log_middleware -from app.mongo.materials import MaterialsStore, create_materials_store -from app.services.audio import AudioService +from app.server.bootstrap import GrpcServers, start_grpc_servers, stop_grpc_servers +from app.server.handboard.audio.service import AudioService +from app.server.handboard.audio.store import MaterialsStore, create_materials_store +from app.server.somni.audio.catalog import AudioCatalogService as SomniAudioService +from app.server.somni.audio.hot import HotTracker +from app.server.somni.quiz.service import QuizService as SomniQuizService +from app.server.somni.report.service import ReportService as SomniReportService from app.services.retrieval import RetrievalService from scripts.sync_es_from_comm import shutdown_sync_scheduler, start_sync_scheduler @dataclass class AppState: - """进程级单例容器,在 lifespan 中填充,供依赖注入读取。""" - settings: Settings es_client: AsyncElasticsearch | None = None + somni_es_client: AsyncElasticsearch | None = None encoder: Encoder | None = None - comm_client: CommClient | None = None materials_store: MaterialsStore | None = None + somni_mongo_client: AsyncIOMotorClient | None = None es_search: EsSearch | None = None es_sync: EsSync | None = None retrieval_service: RetrievalService | None = None audio_service: AudioService | None = None + somni_quiz_service: SomniQuizService | None = None + somni_report_service: SomniReportService | None = None + somni_audio_service: SomniAudioService | None = None search_cache: AudioSearchCache | None = None sleep_stage_cache: SleepStageCandidateCache | None = None + grpc_servers: GrpcServers | None = None _app_state = AppState(settings=get_settings()) @@ -102,20 +112,14 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: _app_state.es_client = es_client encoder = create_encoder(settings) - # debug 模式跳过向量模型加载:本地无外网时可先调 HTTP 路由 if not settings.app_debug: encoder.load() _app_state.encoder = encoder - comm_client = CommClient(settings) - await comm_client.connect() - _app_state.comm_client = comm_client - es_search = EsSearch(es_client, settings) try: await es_search.ensure_indices() except Exception as exc: - # 生产环境 ES 不可达应 fail fast;debug 允许仅验证 API 层 if settings.app_debug: logger.warning("Elasticsearch 不可用(调试模式,继续启动):{}", exc) else: @@ -158,28 +162,67 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: materials_store = create_materials_store(settings) _app_state.materials_store = materials_store if materials_store is None: - logger.warning("未配置 MONGO_URI,定时同步等读 Mongo 路径将不可用") + logger.warning("未配置 MONGO_URI,手板写路径将不可用") _app_state.audio_service = AudioService( - comm_client, + materials_store, es_sync, retrieval, - materials=materials_store, search_cache=search_cache, sleep_stage_cache=sleep_stage_cache, ) + somni_mongo = None + if settings.somni_mongo_uri: + somni_mongo = AsyncIOMotorClient(settings.somni_mongo_uri) + _app_state.somni_mongo_client = somni_mongo + else: + logger.warning("未配置 SOMNI_MONGO_URI,量产问卷与音频查询将不可用") + + _app_state.somni_quiz_service = SomniQuizService(somni_mongo, settings) + _app_state.somni_report_service = SomniReportService(somni_mongo, settings) + somni_es_client = create_es_client( + settings, + node=settings.effective_somni_es_node, + ) + _app_state.somni_es_client = somni_es_client + somni_es_search = EsSearch( + somni_es_client, + settings, + audio_index=settings.somni_es_audio_index, + tag_dictionary_index=settings.somni_es_tag_vectors_index, + ) + somni_redis = await create_somni_redis(settings) + events_store = SearchEventsStore(somni_es_client, settings) + hot_tracker = HotTracker(somni_redis, events_store, settings) + _app_state.somni_audio_service = SomniAudioService( + somni_mongo, + settings, + es_search=somni_es_search, + encoder=encoder, + hot=hot_tracker, + ) + start_sync_scheduler(_app_state, settings) + _app_state.grpc_servers = await start_grpc_servers(_app_state, settings) logger.info("UburNode 音频检索服务已就绪") yield logger.info("正在关闭 UburNode 音频检索服务") + await stop_grpc_servers(_app_state.grpc_servers) + _app_state.grpc_servers = None shutdown_sync_scheduler() + if _app_state.somni_audio_service is not None: + await _app_state.somni_audio_service.drain_hot_tasks() await shutdown_audio_search_cache(search_cache) if materials_store is not None: materials_store.close() - await comm_client.close() + if somni_mongo is not None: + somni_mongo.close() + if somni_redis is not None: + await somni_redis.aclose() + await somni_es_client.close() await es_client.close() @@ -187,7 +230,7 @@ def create_app() -> FastAPI: settings = get_settings() app = FastAPI( title="UburNode Audio Search Service", - description="音频检索服务 — HTTP 对外,gRPC 调 comm-service,ES 索引副本", + description="音频检索服务 — HTTP + 功能手板/量产 gRPC;直连 Mongo,ES 为索引副本", version="0.1.0", lifespan=lifespan, debug=False, @@ -202,7 +245,6 @@ def create_app() -> FastAPI: def run_dev_server() -> None: - """本地开发一键启动(读取 .env 的 APP_HOST / APP_PORT)。""" import uvicorn settings = get_settings() diff --git a/app/mongo/__init__.py b/app/mongo/__init__.py deleted file mode 100644 index 8b87803..0000000 --- a/app/mongo/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Mongo 访问包。""" diff --git a/app/schemas/audio.py b/app/schemas/audio.py index 3f33336..fca7cff 100644 --- a/app/schemas/audio.py +++ b/app/schemas/audio.py @@ -1,4 +1,4 @@ -"""Pydantic 对外契约模型(替代 uburnode_audio.proto)。 +"""Pydantic 对外契约模型(HTTP / gRPC 映射共用)。 字段 snake_case;检索出参直接返回 somni_audio_materials 索引文档(materials 列表)。 创建/更新入参对齐 Mongo Somni 文档结构。 @@ -229,8 +229,8 @@ class AudioMaterialData(BaseModel): evidence_level_tags: list[dict[str, Any]] = Field(default_factory=list) @classmethod - def from_comm_material(cls, material: object) -> Self: - """bionode_comm_pb2.AudioMaterialInfo → HTTP/内存出参。""" + def from_material_like(cls, material: object) -> Self: + """鸭子类型原料对象 → HTTP/内存出参。""" return cls( id=material.id, description=material.description, diff --git a/app/schemas/search_material.py b/app/schemas/search_material.py index 8c0ce7f..103046c 100644 --- a/app/schemas/search_material.py +++ b/app/schemas/search_material.py @@ -10,6 +10,7 @@ def project_search_material(doc: dict[str, Any]) -> dict[str, Any]: "audio_name": str(doc.get("audio_name") or ""), "description": str(doc.get("description") or ""), "audio_url": str(doc.get("audio_url") or ""), + "cover_url": str(doc.get("cover_url") or ""), "content_form_tags": _project_content_form_tags(doc.get("content_form_tags")), "audio_engineering_tags": _project_engineering_tags( doc.get("audio_engineering_tags") diff --git a/app/server/__init__.py b/app/server/__init__.py new file mode 100644 index 0000000..0f9a6c9 --- /dev/null +++ b/app/server/__init__.py @@ -0,0 +1 @@ +"""server 包:功能手板与量产 gRPC / 业务。""" diff --git a/app/server/bootstrap.py b/app/server/bootstrap.py new file mode 100644 index 0000000..cf79b4d --- /dev/null +++ b/app/server/bootstrap.py @@ -0,0 +1,102 @@ +"""双 gRPC 端口启停(功能手板 + 量产)。""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import grpc +from grpc_reflection.v1alpha import reflection +from loguru import logger + +from app.server.handboard.audio.rpc import AudioRpc as HandboardAudioRpc +from app.server.handboard.quiz.rpc import QuizRpc as HandboardQuizRpc +from app.server.somni.audio.rpc import AudioRpc as SomniAudioRpc +from app.server.somni.quiz.rpc import QuizRpc as SomniQuizRpc +from app.server.somni.report.rpc import ReportRpc as SomniReportRpc +from app.uburnode_grpc.grpc_gen import ( + uburnode_pb2, + uburnode_pb2_grpc, + uburnode_somni_pb2, + uburnode_somni_pb2_grpc, +) + +if TYPE_CHECKING: + from app.core.config import Settings + from app.main import AppState + + +@dataclass +class GrpcServers: + handboard: grpc.aio.Server | None = None + somni: grpc.aio.Server | None = None + + +async def start_grpc_servers(state: AppState, settings: Settings) -> GrpcServers: + servers = GrpcServers() + if settings.grpc_enabled: + servers.handboard = await _start_handboard(state, settings) + else: + logger.info("功能手板 gRPC 已关闭(GRPC_ENABLED=false)") + if settings.somni_grpc_enabled: + servers.somni = await _start_somni(state, settings) + else: + logger.info("量产 gRPC 已关闭(SOMNI_GRPC_ENABLED=false)") + return servers + + +async def stop_grpc_servers(servers: GrpcServers | None, *, grace: float = 5.0) -> None: + if servers is None: + return + for name, server in (("handboard", servers.handboard), ("somni", servers.somni)): + if server is None: + continue + await server.stop(grace) + logger.info("{} gRPC Server 已停止", name) + + +async def _start_handboard(state: AppState, settings: Settings) -> grpc.aio.Server: + server = grpc.aio.server() + uburnode_pb2_grpc.add_AudioServiceServicer_to_server( + HandboardAudioRpc(getattr(state, "audio_service", None)), + server, + ) + uburnode_pb2_grpc.add_QuizServiceServicer_to_server(HandboardQuizRpc(), server) + _enable_reflection(server, uburnode_pb2) + bind = f"{settings.grpc_host}:{settings.grpc_port}" + _bind(server, bind, "功能手板") + await server.start() + return server + + +async def _start_somni(state: AppState, settings: Settings) -> grpc.aio.Server: + server = grpc.aio.server() + uburnode_somni_pb2_grpc.add_QuizServiceServicer_to_server( + SomniQuizRpc(getattr(state, "somni_quiz_service", None)), + server, + ) + uburnode_somni_pb2_grpc.add_ReportServiceServicer_to_server( + SomniReportRpc(getattr(state, "somni_report_service", None)), + server, + ) + uburnode_somni_pb2_grpc.add_AudioServiceServicer_to_server( + SomniAudioRpc(getattr(state, "somni_audio_service", None)), + server, + ) + _enable_reflection(server, uburnode_somni_pb2) + bind = f"{settings.grpc_host}:{settings.somni_grpc_port}" + _bind(server, bind, "量产") + await server.start() + return server + + +def _enable_reflection(server: grpc.aio.Server, proto_module) -> None: + names = [reflection.SERVICE_NAME] + names.extend(svc.full_name for svc in proto_module.DESCRIPTOR.services_by_name.values()) + reflection.enable_server_reflection(tuple(names), server) + + +def _bind(server: grpc.aio.Server, bind: str, label: str) -> None: + if server.add_insecure_port(bind) == 0: + raise RuntimeError(f"{label} gRPC 无法绑定 {bind}") + logger.warning("{} gRPC 已以明文监听 {}(无鉴权;勿对公网直接暴露)", label, bind) diff --git a/app/server/errors.py b/app/server/errors.py new file mode 100644 index 0000000..f349597 --- /dev/null +++ b/app/server/errors.py @@ -0,0 +1,66 @@ +"""gRPC 错误映射与统一调用包装。""" + +from __future__ import annotations + +import grpc +from loguru import logger + +from app.core.codes import HttpStatus +from app.core.exceptions import AppError, ElasticsearchUnavailableError + +_STATUS_BY_HTTP: dict[int, grpc.StatusCode] = { + HttpStatus.BAD_REQUEST: grpc.StatusCode.INVALID_ARGUMENT, + HttpStatus.NOT_FOUND: grpc.StatusCode.NOT_FOUND, + HttpStatus.CONFLICT: grpc.StatusCode.ALREADY_EXISTS, + HttpStatus.UNPROCESSABLE_ENTITY: grpc.StatusCode.INVALID_ARGUMENT, + HttpStatus.SERVICE_UNAVAILABLE: grpc.StatusCode.UNAVAILABLE, + HttpStatus.BAD_GATEWAY: grpc.StatusCode.FAILED_PRECONDITION, + 412: grpc.StatusCode.FAILED_PRECONDITION, + HttpStatus.GATEWAY_TIMEOUT: grpc.StatusCode.DEADLINE_EXCEEDED, + HttpStatus.INTERNAL_SERVER_ERROR: grpc.StatusCode.INTERNAL, +} + + +async def abort_invalid(context: grpc.aio.ServicerContext, message: str) -> None: + await context.abort(grpc.StatusCode.INVALID_ARGUMENT, message) + + +async def abort_from_app_error( + context: grpc.aio.ServicerContext, + exc: AppError, +) -> None: + code = _STATUS_BY_HTTP.get(exc.status_code, grpc.StatusCode.INTERNAL) + await context.abort(code, exc.message) + + +async def abort_internal( + context: grpc.aio.ServicerContext, + message: str = "服务器内部错误,请稍后重试", +) -> None: + await context.abort(grpc.StatusCode.INTERNAL, message) + + +async def run_rpc_call(context: grpc.aio.ServicerContext, action): + try: + return await action() + except grpc.aio.AbortError: + raise + except AppError as exc: + await abort_from_app_error(context, exc) + except Exception as exc: + mapped = _maybe_es_error(exc) + if mapped is not None: + await abort_from_app_error(context, mapped) + logger.exception("gRPC 处理失败:{}", exc) + await abort_internal(context) + + +def _maybe_es_error(exc: Exception) -> AppError | None: + try: + from elastic_transport import TransportError + from elasticsearch import ApiError + except ImportError: # pragma: no cover + return None + if isinstance(exc, (TransportError, ApiError)): + return ElasticsearchUnavailableError(str(exc)) + return None diff --git a/app/server/handboard/__init__.py b/app/server/handboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/handboard/audio/__init__.py b/app/server/handboard/audio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/handboard/audio/mapper.py b/app/server/handboard/audio/mapper.py new file mode 100644 index 0000000..aedf7dc --- /dev/null +++ b/app/server/handboard/audio/mapper.py @@ -0,0 +1,97 @@ +"""功能手板 audio proto ↔ Pydantic。""" + +from __future__ import annotations + +from typing import Any + +from google.protobuf.json_format import MessageToDict, ParseDict +from google.protobuf.struct_pb2 import Struct +from pydantic import ValidationError + +from app.core.codes import HttpStatus +from app.core.exceptions import AppError +from app.schemas.audio import ( + CreateAudioRequest, + SearchAudioData, + SearchAudioRequest, + UpdateAudioRequest, +) +from app.uburnode_grpc.grpc_gen import uburnode_pb2 + + +class GrpcValidationError(AppError): + def __init__(self, message: str) -> None: + super().__init__(message=message, status_code=HttpStatus.UNPROCESSABLE_ENTITY) + + +def create_req_to_pydantic(req: uburnode_pb2.CreateAudioReq) -> CreateAudioRequest: + return _parse_create(_message_to_request_dict(req)) + + +def update_req_to_pydantic( + req: uburnode_pb2.UpdateAudioReq, +) -> tuple[str, UpdateAudioRequest]: + if not req.material_id.strip(): + raise GrpcValidationError("material_id 不能为空") + payload = _message_to_request_dict(req, exclude=("material_id",)) + try: + body = UpdateAudioRequest.model_validate(payload) + except ValidationError as exc: + raise GrpcValidationError(_format_validation(exc)) from exc + return req.material_id, body + + +def search_req_to_pydantic(req: uburnode_pb2.SearchAudioReq) -> SearchAudioRequest: + payload: dict[str, Any] = { + "sleep_stage_tags": list(req.sleep_stage_tags), + "content_tags": list(req.content_tags), + "disliked_tags": list(req.disliked_tags), + } + if req.HasField("query_text"): + payload["query_text"] = req.query_text + if req.HasField("top_k"): + payload["top_k"] = req.top_k + try: + return SearchAudioRequest.model_validate(payload) + except ValidationError as exc: + raise GrpcValidationError(_format_validation(exc)) from exc + + +def dict_to_audio_material_res(data: dict[str, Any]) -> uburnode_pb2.AudioMaterialRes: + material = uburnode_pb2.AudioMaterial() + ParseDict(data, material, ignore_unknown_fields=True) + return uburnode_pb2.AudioMaterialRes(material=material) + + +def search_data_to_res(data: SearchAudioData) -> uburnode_pb2.SearchAudioRes: + res = uburnode_pb2.SearchAudioRes() + for item in data.materials: + struct = Struct() + struct.update(item if isinstance(item, dict) else {}) + res.materials.append(struct) + return res + + +def ok_operation(msg: str = "ok") -> uburnode_pb2.OperationResponse: + return uburnode_pb2.OperationResponse(ok=True, msg=msg) + + +def _parse_create(payload: dict[str, Any]) -> CreateAudioRequest: + try: + return CreateAudioRequest.model_validate(payload) + except ValidationError as exc: + raise GrpcValidationError(_format_validation(exc)) from exc + + +def _message_to_request_dict(msg: object, exclude: tuple[str, ...] = ()) -> dict[str, Any]: + raw = MessageToDict(msg, preserving_proto_field_name=True) # type: ignore[arg-type] + for key in exclude: + raw.pop(key, None) + return raw + + +def _format_validation(exc: ValidationError) -> str: + return "; ".join( + f"{'.'.join(str(p) for p in err.get('loc', []))}: {err.get('msg', '')}" + for err in exc.errors() + ) diff --git a/app/server/handboard/audio/rpc.py b/app/server/handboard/audio/rpc.py new file mode 100644 index 0000000..8c0d5dc --- /dev/null +++ b/app/server/handboard/audio/rpc.py @@ -0,0 +1,66 @@ +"""功能手板音频 gRPC 适配。""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from app.core.exceptions import ServiceNotReadyError +from app.server.errors import abort_from_app_error, abort_invalid, run_rpc_call +from app.server.handboard.audio import mapper as audio_mapper +from app.uburnode_grpc.grpc_gen import uburnode_pb2, uburnode_pb2_grpc + +if TYPE_CHECKING: + import grpc + + from app.server.handboard.audio.service import AudioService + + +class AudioRpc(uburnode_pb2_grpc.AudioServiceServicer): + def __init__(self, audio_service: AudioService | None) -> None: + self._audio = audio_service + + async def CreateAudio(self, request, context): + service = await self._require_service(context) + + async def _do(): + body = audio_mapper.create_req_to_pydantic(request) + data = await service.create_audio(body) + return audio_mapper.dict_to_audio_material_res(data) + + return await run_rpc_call(context, _do) + + async def UpdateAudio(self, request, context): + service = await self._require_service(context) + + async def _do(): + material_id, body = audio_mapper.update_req_to_pydantic(request) + await service.update_audio(material_id, body) + return audio_mapper.ok_operation("更新成功") + + return await run_rpc_call(context, _do) + + async def DeleteAudio(self, request, context): + if not request.id.strip(): + await abort_invalid(context, "id 不能为空") + service = await self._require_service(context) + + async def _do(): + await service.delete_audio(request.id) + return audio_mapper.ok_operation("删除成功") + + return await run_rpc_call(context, _do) + + async def SearchAudio(self, request, context): + service = await self._require_service(context) + + async def _do(): + body = audio_mapper.search_req_to_pydantic(request) + data = await service.search_audio(body) + return audio_mapper.search_data_to_res(data) + + return await run_rpc_call(context, _do) + + async def _require_service(self, context: grpc.aio.ServicerContext) -> AudioService: + if self._audio is None: + await abort_from_app_error(context, ServiceNotReadyError()) + return self._audio # type: ignore[return-value] diff --git a/app/services/audio.py b/app/server/handboard/audio/service.py similarity index 68% rename from app/services/audio.py rename to app/server/handboard/audio/service.py index 6dc2813..4d30502 100644 --- a/app/services/audio.py +++ b/app/server/handboard/audio/service.py @@ -1,8 +1,4 @@ -"""音频业务编排层(AudioService)。 - -写路径:HTTP → comm gRPC(Create/Update/Delete)→ EsSync -读路径:HTTP → 检索缓存 → RetrievalService → ES -""" +"""功能手板音频业务:直连 Mongo + 本侧 ES / 缓存。""" from __future__ import annotations @@ -10,23 +6,21 @@ from loguru import logger -from app.bionode_grpc_clients import CommClient from app.cache.audio_search_cache import AudioSearchCache from app.cache.sleep_stage_cache import SleepStageCandidateCache from app.cache.sleep_stage_refresh import DebouncedSleepStageCacheRefresh from app.core.config import Settings -from app.core.exceptions import CommMaterialNotFoundError +from app.core.exceptions import MongoNotConfiguredError from app.es.sync import EsSync -from app.mongo.materials import MaterialsStore from app.schemas.audio import ( CreateAudioRequest, SearchAudioData, SearchAudioRequest, UpdateAudioRequest, ) +from app.server.handboard.audio.store import MaterialsStore from app.services.retrieval import RetrievalService -# 与 Mongo 写入默认对齐,保证创建 HTTP 响应字段形状不变 _CREATE_RESPONSE_DEFAULTS: dict[str, Any] = { "status": True, "audio_url": "", @@ -41,23 +35,21 @@ class AudioService: - """编排 CUD + Search。""" + """编排 CUD + Search(无 BioNode)。""" def __init__( self, - comm: CommClient, + materials: MaterialsStore | None, es_sync: EsSync, retrieval: RetrievalService, - materials: MaterialsStore | None = None, search_cache: AudioSearchCache | None = None, sleep_stage_cache: SleepStageCandidateCache | None = None, *, sleep_stage_rewarm_delay_sec: float | None = None, ) -> None: - self._comm = comm + self._materials = materials self._es_sync = es_sync self._retrieval = retrieval - self._materials = materials self._search_cache = search_cache self._sleep_stage_cache = sleep_stage_cache delay = ( @@ -71,23 +63,28 @@ def __init__( delay_sec=delay, ) + def _require_store(self) -> MaterialsStore: + if self._materials is None: + raise MongoNotConfiguredError() + return self._materials + async def create_audio(self, request: CreateAudioRequest) -> dict[str, Any]: - await self._comm.create_audio_material(request) - material_id = await self._resolve_created_id(request.audio_name) - saved = _create_response_doc(material_id, request) - await self._es_sync.upsert_somni_material(material_id, saved) + store = self._require_store() + saved = await store.insert_material(request.to_mongo_doc()) + await self._es_sync.upsert_somni_material(saved["id"], saved) await self._invalidate_candidate_caches() - logger.info("已创建音频原料,id={}", material_id) - return saved + logger.info("已创建音频原料,id={}", saved["id"]) + return {**_CREATE_RESPONSE_DEFAULTS, **saved} async def update_audio(self, material_id: str, request: UpdateAudioRequest) -> None: - await self._comm.update_audio_material(material_id, request) - saved = {"id": material_id, **request.model_dump(exclude_unset=True)} + store = self._require_store() + saved = await store.update_material(material_id, request.to_update_fields()) await self._es_sync.upsert_somni_material(material_id, saved) await self._invalidate_candidate_caches() async def delete_audio(self, material_id: str) -> None: - await self._comm.delete_audio_material(material_id) + store = self._require_store() + await store.delete_material(material_id) await self._es_sync.delete_audio(material_id) await self._invalidate_candidate_caches() @@ -96,17 +93,10 @@ async def search_audio(self, request: SearchAudioRequest) -> SearchAudioData: if cached is not None: return SearchAudioData(materials=cached) results = await self._retrieval.search(request) - # 空结果不写入缓存,避免长时间缓存「无命中」导致误伤 if results: await self._set_cached_materials(request, results) return SearchAudioData(materials=results) - async def _resolve_created_id(self, audio_name: str) -> str: - materials = await self._comm.list_audio_materials_by_name(audio_name) - if not materials: - raise CommMaterialNotFoundError(audio_name) - return materials[0].id - async def _get_cached_materials( self, request: SearchAudioRequest ) -> list[dict[str, Any]] | None: @@ -137,12 +127,5 @@ async def _clear_search_cache(self) -> None: logger.error("清除检索缓存失败:{}", exc) async def _invalidate_candidate_caches(self) -> None: - """CUD 后立即清缓存;睡眠阶段候选延时去抖重建,避免频繁写入反复打 ES。""" await self._clear_search_cache() await self._sleep_stage_refresh.invalidate() - - -def _create_response_doc(material_id: str, request: CreateAudioRequest) -> dict[str, Any]: - payload = {**_CREATE_RESPONSE_DEFAULTS, **request.to_mongo_doc()} - payload["id"] = material_id - return payload diff --git a/app/mongo/materials.py b/app/server/handboard/audio/store.py similarity index 66% rename from app/mongo/materials.py rename to app/server/handboard/audio/store.py index 14a8553..cc25e0d 100644 --- a/app/mongo/materials.py +++ b/app/server/handboard/audio/store.py @@ -1,20 +1,17 @@ -"""Mongo somni_audio_materials 读写。""" +"""手板 Mongo 原料集合访问(直连,非独立 mongo 包)。""" from __future__ import annotations -from datetime import UTC, datetime from typing import Any -from bson import ObjectId -from bson.errors import InvalidId from loguru import logger from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection from pymongo import ReturnDocument +from app.core.bson_util import bson_to_jsonable, parse_object_id, utc_now from app.core.config import Settings from app.core.exceptions import MaterialNotFoundError -# Mongo $jsonSchema required;HTTP 侧仅 audio_name 必填时由此补齐 _CREATE_REQUIRED_DEFAULTS: dict[str, Any] = { "status": True, "audio_url": "", @@ -28,28 +25,6 @@ } -def utc_now() -> datetime: - """Mongo 校验要求 created_at / updated_at 为 BSON date。""" - return datetime.now(UTC) - - -def utc_now_iso() -> str: - return utc_now().isoformat().replace("+00:00", "Z") - - -def bson_to_jsonable(value: Any) -> Any: - """BSON → JSON 可序列化(HTTP / ES)。""" - if isinstance(value, ObjectId): - return str(value) - if isinstance(value, datetime): - return value.astimezone(UTC).isoformat().replace("+00:00", "Z") - if isinstance(value, dict): - return {k: bson_to_jsonable(v) for k, v in value.items()} - if isinstance(value, list): - return [bson_to_jsonable(v) for v in value] - return value - - class MaterialsStore: """somni_audio_materials 集合访问。""" @@ -63,7 +38,6 @@ def _collection(self) -> AsyncIOMotorCollection: return db[self._settings.mongo_materials_collection] async def insert_material(self, doc: dict[str, Any]) -> dict[str, Any]: - """插入原料,返回含 id 的 JSON 文档。""" now = utc_now() payload = {**_CREATE_REQUIRED_DEFAULTS, **doc} payload["created_at"] = doc.get("created_at") or now @@ -78,8 +52,7 @@ async def update_material( material_id: str, fields: dict[str, Any], ) -> dict[str, Any]: - """按字段 `$set` 更新;fields 为空则直接返回当前文档。""" - oid = self._parse_object_id(material_id) + oid = parse_object_id(material_id) if not fields: return await self.get_material(material_id) payload = {**fields, "updated_at": utc_now()} @@ -93,8 +66,15 @@ async def update_material( logger.info("Mongo 已更新原料,id={}", material_id) return self._as_response(doc, material_id) + async def delete_material(self, material_id: str) -> None: + oid = parse_object_id(material_id) + result = await self._collection.delete_one({"_id": oid}) + if result.deleted_count == 0: + raise MaterialNotFoundError(material_id) + logger.info("Mongo 已删除原料,id={}", material_id) + async def get_material(self, material_id: str) -> dict[str, Any]: - oid = self._parse_object_id(material_id) + oid = parse_object_id(material_id) doc = await self._collection.find_one({"_id": oid}) if doc is None: raise MaterialNotFoundError(material_id) @@ -109,16 +89,8 @@ def _as_response(self, doc: dict[str, Any], material_id: str) -> dict[str, Any]: payload["id"] = material_id return payload - @staticmethod - def _parse_object_id(material_id: str) -> ObjectId: - try: - return ObjectId(material_id) - except InvalidId as exc: - raise MaterialNotFoundError(material_id) from exc - def create_materials_store(settings: Settings) -> MaterialsStore | None: - """有 mongo_uri 时创建 store,否则返回 None。""" if not settings.mongo_uri: return None client = AsyncIOMotorClient(settings.mongo_uri) diff --git a/app/server/handboard/quiz/__init__.py b/app/server/handboard/quiz/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/handboard/quiz/rpc.py b/app/server/handboard/quiz/rpc.py new file mode 100644 index 0000000..36add57 --- /dev/null +++ b/app/server/handboard/quiz/rpc.py @@ -0,0 +1,17 @@ +"""功能手板问卷 gRPC 适配。""" + +from __future__ import annotations + +from app.server.errors import abort_invalid, run_rpc_call +from app.uburnode_grpc.grpc_gen import uburnode_pb2, uburnode_pb2_grpc + + +class QuizRpc(uburnode_pb2_grpc.QuizServiceServicer): + async def GetAnswer(self, request, context): + if not request.uid.strip() or not request.answer_id.strip(): + await abort_invalid(context, "uid 与 answer_id 均不能为空") + + async def _do(): + return uburnode_pb2.GetAnswerRes() + + return await run_rpc_call(context, _do) diff --git a/app/server/handboard/quiz/service.py b/app/server/handboard/quiz/service.py new file mode 100644 index 0000000..f7f7634 --- /dev/null +++ b/app/server/handboard/quiz/service.py @@ -0,0 +1,9 @@ +"""功能手板问卷业务(本期 GetAnswer stub)。""" + +from __future__ import annotations + + +class QuizService: + async def get_answer(self, uid: str, answer_id: str) -> dict: + _ = (uid, answer_id) + return {} diff --git a/app/server/somni/__init__.py b/app/server/somni/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/somni/audio/__init__.py b/app/server/somni/audio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/somni/audio/catalog.py b/app/server/somni/audio/catalog.py new file mode 100644 index 0000000..c266ebb --- /dev/null +++ b/app/server/somni/audio/catalog.py @@ -0,0 +1,355 @@ +"""量产音频目录查询:标签词典 + 音频原料。""" + +from __future__ import annotations + +import asyncio +import math +from time import monotonic +from typing import Any + +from loguru import logger +from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection + +from app.core.bson_util import bson_to_jsonable +from app.core.codes import HttpStatus +from app.core.config import Settings +from app.core.exceptions import AppError, EncoderNotReadyError +from app.embedding.encoder import Encoder +from app.es.search import EsSearch +from app.server.somni.audio.hot import HotTracker + +_TAG_ENABLED = "启用" +_CONTENT_FORM = "content_form" + + +class InvalidAudioQueryError(AppError): + def __init__(self, message: str) -> None: + super().__init__(message=message, status_code=HttpStatus.BAD_REQUEST) + + +class AudioCatalogService: + def __init__( + self, + client: AsyncIOMotorClient | None, + settings: Settings, + *, + es_search: EsSearch | None = None, + encoder: Encoder | None = None, + hot: HotTracker | None = None, + ) -> None: + self._client = client + self._settings = settings + self._es_search = es_search + self._encoder = encoder + self._hot = hot + self._audio_cache: dict[bool, tuple[float, list[dict[str, Any]]]] = {} + self._audio_cache_lock = asyncio.Lock() + self._hot_tasks: set[asyncio.Task[None]] = set() + self._hot_sem = asyncio.Semaphore(32) + + async def get_audio_tag(self) -> dict[str, Any]: + collection = self._tags() + query = _root_tag_query() + total = await collection.count_documents(query) + self._reject_over_limit(total) + cursor = collection.find( + query, + { + "_id": 1, + "id": 1, + "type": 1, + "code": 1, + "name": 1, + "name_en": 1, + "parent_tag_id": 1, + "status": 1, + }, + ) + docs = [bson_to_jsonable(doc) async for doc in cursor] + return {"tags": [_map_tag_dict(doc) for doc in docs]} + + async def get_audio( + self, + *, + page: int | None, + page_size: int | None, + fetch_all: bool, + query_text: str, + tag_code: str, + ) -> dict[str, Any]: + text = query_text.strip() + code = tag_code.strip() + docs = await self._load_audios(from_es=bool(text)) + if code: + docs = [doc for doc in docs if _has_content_form_code(doc, code)] + if text: + tag_ids = await self._content_form_tag_ids_by_text(text) + docs = [doc for doc in docs if _has_content_form_tag_id(doc, tag_ids)] + payload = _paginate_docs(docs, page, page_size, fetch_all, self._settings) + payload["list"] = [_to_audio_list_item(item) for item in payload["list"]] + self._schedule_hot(query_text, int(payload.get("total") or 0)) + return payload + + async def get_hot(self) -> dict[str, Any]: + if self._hot is None: + raise AppError( + message="量产 Redis 未配置,无法获取热点", + status_code=HttpStatus.SERVICE_UNAVAILABLE, + ) + return {"items": await self._hot.list_hot()} + + def _schedule_hot(self, query_text: str, hit_count: int) -> None: + if self._hot is None or not query_text.strip(): + return + task = asyncio.create_task(self._record_hot_safely(query_text, hit_count)) + self._hot_tasks.add(task) + task.add_done_callback(self._hot_tasks.discard) + + async def drain_hot_tasks(self, *, timeout_sec: float = 5.0) -> None: + """关闭前排空热点记账任务,避免访问已关闭的 Redis/ES。""" + pending = [task for task in self._hot_tasks if not task.done()] + if not pending: + return + done, still = await asyncio.wait(pending, timeout=max(0.1, timeout_sec)) + for task in still: + task.cancel() + if still: + await asyncio.gather(*still, return_exceptions=True) + logger.warning("量产热点后台任务关闭超时,已取消 {} 个", len(still)) + _ = done + + async def _record_hot_safely(self, query_text: str, hit_count: int) -> None: + async with self._hot_sem: + try: + await self._hot.record_search(query_text, hit_count=hit_count) + except Exception as exc: + logger.warning("量产热点后台记账失败:{}", exc) + + async def _load_audios(self, *, from_es: bool) -> list[dict[str, Any]]: + now = monotonic() + cached = self._audio_cache.get(from_es) + if cached is not None and self._is_cache_fresh(cached[0], now): + return cached[1] + async with self._audio_cache_lock: + cached = self._audio_cache.get(from_es) + if cached is not None and self._is_cache_fresh(cached[0], now): + return cached[1] + raw = await self._fetch_audios_es() if from_es else await self._fetch_audios_mongo() + docs = [_map_material(doc) for doc in raw] + self._audio_cache[from_es] = (now, docs) + return docs + + def _is_cache_fresh(self, loaded_at: float, now: float) -> bool: + ttl = self._settings.somni_audio_catalog_cache_ttl_sec + return ttl > 0 and now - loaded_at < ttl + + async def _fetch_audios_mongo(self) -> list[dict[str, Any]]: + collection = self._materials() + total = await collection.count_documents({}) + self._reject_over_limit(total) + cursor = collection.find({}, {"embedding": 0}) + return [bson_to_jsonable(doc) async for doc in cursor] + + async def _fetch_audios_es(self) -> list[dict[str, Any]]: + if self._es_search is None: + raise AppError( + message="Elasticsearch 未就绪,无法按搜索词查询音频", + status_code=HttpStatus.SERVICE_UNAVAILABLE, + ) + docs = await self._es_search.list_audio_catalog_docs( + size=self._settings.fetch_all_hard_limit + 1, + ) + self._reject_over_limit(len(docs)) + return docs + + async def _content_form_tag_ids_by_text(self, text: str) -> set[str]: + if self._encoder is None or not self._encoder.is_loaded: + raise EncoderNotReadyError() + if self._es_search is None: + raise AppError( + message="Elasticsearch 未就绪,无法按搜索词匹配标签", + status_code=HttpStatus.SERVICE_UNAVAILABLE, + ) + query_vector = await self._encoder.encode_one(text) + tags = await self._es_search.list_content_tag_vectors() + threshold = self._settings.get_audio_root_tag_sim_threshold + scored: list[tuple[float, str]] = [] + for tag in tags: + if not _is_content_form_dict(tag): + continue + vector = tag.get("vector") + if not isinstance(vector, list) or not vector: + continue + sim = _cosine_similarity(query_vector, vector) + if sim <= threshold: + continue + tag_id = str(tag.get("id") or "").strip() + if tag_id: + scored.append((sim, tag_id)) + return _select_matched_tag_ids(scored) + + def _tags(self) -> AsyncIOMotorCollection: + return self._db()[self._settings.somni_mongo_tag_dictionary_collection] + + def _materials(self) -> AsyncIOMotorCollection: + return self._db()[self._settings.somni_mongo_materials_collection] + + def _db(self): + if self._client is None: + raise AppError( + message="量产 Mongo 未配置(SOMNI_MONGO_URI),无法查询音频", + status_code=HttpStatus.SERVICE_UNAVAILABLE, + ) + return self._client[self._settings.somni_mongo_db] + + def _reject_over_limit(self, total: int) -> None: + limit = self._settings.fetch_all_hard_limit + if total > limit: + raise InvalidAudioQueryError(f"全量条数超过上限 {limit}") + + +def _select_matched_tag_ids(scored: list[tuple[float, str]]) -> set[str]: + """保留高分标签;近精确命中时收紧范围,避免宽泛根标签稀释结果。""" + if not scored: + return set() + best = max(sim for sim, _ in scored) + if best >= 0.9: + return {tag_id for sim, tag_id in scored if sim >= best - 0.05} + return {tag_id for _, tag_id in scored} + + +def _root_tag_query() -> dict[str, Any]: + return { + "type": _CONTENT_FORM, + "status": _TAG_ENABLED, + "$or": [ + {"parent_tag_id": {"$exists": False}}, + {"parent_tag_id": None}, + {"parent_tag_id": ""}, + ], + } + + +def _paginate_docs( + docs: list[dict[str, Any]], + page: int | None, + page_size: int | None, + fetch_all: bool, + settings: Settings, +) -> dict[str, Any]: + total = len(docs) + if fetch_all: + if total > settings.fetch_all_hard_limit: + raise InvalidAudioQueryError(f"全量条数超过上限 {settings.fetch_all_hard_limit}") + return {"list": docs, "page": 1, "page_size": len(docs), "total": total} + cur_page, size = _page_window(page, page_size, settings) + start = (cur_page - 1) * size + chunk = docs[start : start + size] + return {"list": chunk, "page": cur_page, "page_size": size, "total": total} + + +def _page_window( + page: int | None, + page_size: int | None, + settings: Settings, +) -> tuple[int, int]: + cur_page = 1 if page is None else page + size = settings.default_page_size if page_size is None else page_size + if cur_page < 1 or size < 1: + raise InvalidAudioQueryError("page / page_size 须 ≥ 1") + return cur_page, min(size, settings.max_page_size) + + +def _map_tag_dict(doc: dict[str, Any]) -> dict[str, Any]: + parent = doc.get("parent_tag_id") + return { + "type": str(doc.get("type") or ""), + "code": str(doc.get("code") or ""), + "name": str(doc.get("name") or ""), + "name_en": str(doc.get("name_en") or ""), + "id": str(doc.get("id") or doc.get("_id") or ""), + "parent_tag_id": None if parent is None else str(parent), + "status": str(doc.get("status") or ""), + } + + +def _map_material(doc: dict[str, Any]) -> dict[str, Any]: + """缓存/过滤用中间形态,保留 content_form_tags。""" + return { + "id": str(doc.get("id") or doc.get("_id") or ""), + "audio_name": str(doc.get("audio_name") or ""), + "audio_url": str(doc.get("audio_url") or ""), + "cover_url": str(doc.get("cover_url") or ""), + "description": str(doc.get("description") or ""), + "vip": _to_vip(doc.get("vip")), + "content_form_tags": doc.get("content_form_tags") or [], + } + + +def _to_audio_list_item(doc: dict[str, Any]) -> dict[str, Any]: + return { + "id": str(doc.get("id") or ""), + "audio_name": str(doc.get("audio_name") or ""), + "audio_url": str(doc.get("audio_url") or ""), + "cover_url": str(doc.get("cover_url") or ""), + "description": str(doc.get("description") or ""), + "vip": _to_vip(doc.get("vip")), + } + + +def _to_vip(value: Any) -> int: + """库无 vip / 假值时返回 0;真值返回 1(兼容 bool/int/常见字符串)。""" + if value is None or value is False: + return 0 + if isinstance(value, (int, float)): + return 1 if value != 0 else 0 + if isinstance(value, str): + normalized = value.strip().lower() + if normalized in {"", "0", "false", "no", "off", "none", "null"}: + return 0 + if normalized in {"1", "true", "yes", "on"}: + return 1 + return 0 + return 1 if bool(value) else 0 + + +def _is_blank(value: Any) -> bool: + return value is None or str(value).strip() in ("", "None") + + +def _is_content_form_dict(tag: dict[str, Any]) -> bool: + dimension = str(tag.get("dimension") or tag.get("type") or "") + return dimension == _CONTENT_FORM + + +def _is_root_content_form_dict(tag: dict[str, Any]) -> bool: + return _is_content_form_dict(tag) and _is_blank(tag.get("parent_tag_id")) + + +def _has_content_form_code(doc: dict[str, Any], tag_code: str) -> bool: + for item in doc.get("content_form_tags") or []: + if isinstance(item, dict) and str(item.get("code") or "") == tag_code: + return True + return False + + +def _has_content_form_tag_id(doc: dict[str, Any], tag_ids: set[str]) -> bool: + if not tag_ids: + return False + for item in doc.get("content_form_tags") or []: + if not isinstance(item, dict): + continue + if str(item.get("tag_id") or "") in tag_ids: + return True + return False + + +def _cosine_similarity(left: list[float], right: list[float]) -> float: + if len(left) != len(right) or not left: + return 0.0 + dot = sum(x * y for x, y in zip(left, right, strict=True)) + norm_left = math.sqrt(sum(x * x for x in left)) + norm_right = math.sqrt(sum(y * y for y in right)) + if norm_left == 0 or norm_right == 0: + return 0.0 + return dot / (norm_left * norm_right) diff --git a/app/server/somni/audio/hot.py b/app/server/somni/audio/hot.py new file mode 100644 index 0000000..c9fba83 --- /dev/null +++ b/app/server/somni/audio/hot.py @@ -0,0 +1,93 @@ +"""量产音频搜索热点:Redis 计数 + ES 明细。""" + +from __future__ import annotations + +from typing import Any + +from loguru import logger + +from app.core.codes import HttpStatus +from app.core.config import Settings +from app.core.exceptions import AppError +from app.es.search_events import SearchEventsStore + + +def normalize_keyword(text: str) -> str: + return text.strip() + + +def _as_str(member: bytes | str) -> str: + if isinstance(member, bytes): + return member.decode() + return member + + +class HotTracker: + def __init__( + self, + redis: Any, + events_store: SearchEventsStore | None, + settings: Settings, + ) -> None: + self._redis = redis + self._events = events_store + self._settings = settings + + async def record_search(self, raw_query: str, *, hit_count: int) -> None: + if not self._settings.somni_hot_enabled: + return + keyword = normalize_keyword(raw_query) + if not keyword: + return + await self._safe_redis_incr(keyword) + await self._safe_es_index(keyword, raw_query, hit_count) + + async def list_hot(self) -> list[dict[str, Any]]: + if not self._settings.somni_hot_enabled: + return [] + if self._settings.somni_hot_top_n <= 0: + return [] + if self._redis is None: + raise AppError( + message="量产 Redis 未配置,无法获取热点", + status_code=HttpStatus.SERVICE_UNAVAILABLE, + ) + try: + rows = await self._redis.zrevrange( + self._settings.somni_hot_redis_key, + 0, + self._settings.somni_hot_top_n - 1, + withscores=True, + ) + except Exception as exc: + logger.warning("量产热点 Redis 读取失败:{}", exc) + raise AppError( + message="量产 Redis 不可用,无法获取热点", + status_code=HttpStatus.SERVICE_UNAVAILABLE, + ) from exc + return [{"keyword": _as_str(member), "score": int(score)} for member, score in rows] + + async def _safe_redis_incr(self, keyword: str) -> None: + if self._redis is None: + return + try: + await self._redis.zincrby(self._settings.somni_hot_redis_key, 1, keyword) + except Exception as exc: + logger.warning("量产热点 Redis 写入失败:{}", exc) + + async def _safe_es_index( + self, + keyword: str, + raw_query: str, + hit_count: int, + ) -> None: + if self._events is None: + return + try: + await self._events.index_event( + keyword=keyword, + raw_query=raw_query, + hit_count=hit_count, + ) + except Exception as exc: + logger.warning("量产热点 ES 写入失败:{}", exc) diff --git a/app/server/somni/audio/rpc.py b/app/server/somni/audio/rpc.py new file mode 100644 index 0000000..849c4ab --- /dev/null +++ b/app/server/somni/audio/rpc.py @@ -0,0 +1,121 @@ +"""量产音频 gRPC 适配。""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from google.protobuf.struct_pb2 import Value + +from app.core.exceptions import ServiceNotReadyError +from app.server.errors import abort_from_app_error, run_rpc_call +from app.uburnode_grpc.grpc_gen import uburnode_somni_pb2, uburnode_somni_pb2_grpc + +if TYPE_CHECKING: + from app.server.somni.audio.catalog import AudioCatalogService + + +class AudioRpc(uburnode_somni_pb2_grpc.AudioServiceServicer): + def __init__(self, service: AudioCatalogService | None) -> None: + self._service = service + + async def GetAudio(self, request, context): + service = await self._require(context) + + async def _do(): + payload = await service.get_audio(**_get_audio_kwargs(request)) + return _to_audio_res(payload) + + return await run_rpc_call(context, _do) + + async def GetAudioTag(self, request, context): + del request + service = await self._require(context) + + async def _do(): + payload = await service.get_audio_tag() + return _to_tag_res(payload) + + return await run_rpc_call(context, _do) + + async def GetHot(self, request, context): + del request + service = await self._require(context) + + async def _do(): + payload = await service.get_hot() + return _to_hot_res(payload) + + return await run_rpc_call(context, _do) + + async def _require(self, context) -> AudioCatalogService: + if self._service is None: + await abort_from_app_error(context, ServiceNotReadyError()) + return self._service # type: ignore[return-value] + + +def _get_audio_kwargs(request) -> dict[str, Any]: + return { + "page": request.page if request.HasField("page") else None, + "page_size": request.page_size if request.HasField("page_size") else None, + "fetch_all": bool(request.fetch_all) if request.HasField("fetch_all") else False, + "query_text": request.query_text if request.HasField("query_text") else "", + "tag_code": request.tag_code if request.HasField("tag_code") else "", + } + + +def _to_tag_res(payload: dict[str, Any]) -> uburnode_somni_pb2.GetAudioTagRes: + res = uburnode_somni_pb2.GetAudioTagRes() + for item in payload.get("tags") or []: + res.tags.append( + uburnode_somni_pb2.TagDictItem( + type=str(item.get("type") or ""), + code=str(item.get("code") or ""), + name=str(item.get("name") or ""), + name_en=str(item.get("name_en") or ""), + id=str(item.get("id") or ""), + parent_tag_id=_to_value(item.get("parent_tag_id")), + status=str(item.get("status") or ""), + ) + ) + return res + + +def _to_value(value: Any) -> Value: + result = Value() + if value is None: + result.null_value = 0 + else: + result.string_value = str(value) + return result + + +def _to_hot_res(payload: dict[str, Any]) -> uburnode_somni_pb2.GetHotRes: + res = uburnode_somni_pb2.GetHotRes() + for item in payload.get("items") or []: + res.items.append( + uburnode_somni_pb2.HotKeyword( + keyword=str(item.get("keyword") or ""), + score=int(item.get("score") or 0), + ) + ) + return res + + +def _to_audio_res(payload: dict[str, Any]) -> uburnode_somni_pb2.GetAudioRes: + res = uburnode_somni_pb2.GetAudioRes( + page=int(payload.get("page") or 1), + page_size=int(payload.get("page_size") or 0), + total=int(payload.get("total") or 0), + ) + for item in payload.get("list") or []: + res.list.append( + uburnode_somni_pb2.AudioListItem( + id=str(item.get("id") or ""), + audio_name=str(item.get("audio_name") or ""), + audio_url=str(item.get("audio_url") or ""), + cover_url=str(item.get("cover_url") or ""), + description=str(item.get("description") or ""), + vip=int(item.get("vip") or 0), + ) + ) + return res diff --git a/app/server/somni/audio/service.py b/app/server/somni/audio/service.py new file mode 100644 index 0000000..224a361 --- /dev/null +++ b/app/server/somni/audio/service.py @@ -0,0 +1,225 @@ +"""量产音频业务:标签 / 列表 / 文本搜索(直连 Somni Mongo + 量产 ES)。""" + +from __future__ import annotations + +import math +from typing import Any + +from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection + +from app.core.bson_util import bson_to_jsonable +from app.core.config import Settings +from app.core.exceptions import AppError, MongoNotConfiguredError +from app.core.codes import HttpStatus +from app.schemas.audio import SearchAudioData, SearchAudioRequest +from app.services.retrieval import RetrievalService + + +class InvalidListParamsError(AppError): + def __init__(self, message: str) -> None: + super().__init__(message=message, status_code=HttpStatus.BAD_REQUEST) + + +class SomniAudioService: + def __init__( + self, + client: AsyncIOMotorClient | None, + settings: Settings, + retrieval: RetrievalService | None = None, + ) -> None: + self._client = client + self._settings = settings + self._retrieval = retrieval + + def _require_client(self) -> AsyncIOMotorClient: + if self._client is None: + raise MongoNotConfiguredError() + return self._client + + def _materials(self) -> AsyncIOMotorCollection: + client = self._require_client() + db = client[self._settings.somni_mongo_db] + return db[self._settings.somni_mongo_materials_collection] + + def _tags(self) -> AsyncIOMotorCollection: + client = self._require_client() + db = client[self._settings.somni_mongo_db] + return db[self._settings.somni_mongo_tag_dictionary_collection] + + async def list_tags( + self, + *, + page: int | None = None, + page_size: int | None = None, + fetch_all: bool = False, + type_: str | None = None, + enabled_only: bool = False, + level: int = 0, + ) -> dict[str, Any]: + if level not in (0, 1, 2): + raise InvalidListParamsError("level 仅支持 0 / 1 / 2") + query: dict[str, Any] = {} + if type_: + query["type"] = type_ + if enabled_only: + query["status"] = "启用" + if level == 1: + query["$or"] = [ + {"parent_tag_id": {"$exists": False}}, + {"parent_tag_id": None}, + {"parent_tag_id": ""}, + ] + elif level == 2: + query["parent_tag_id"] = {"$nin": [None, ""]} + return await self._paginate( + self._tags(), + query, + page=page, + page_size=page_size, + fetch_all=fetch_all, + map_doc=self._map_tag, + list_key="tags", + ) + + async def list_audios( + self, + *, + page: int | None = None, + page_size: int | None = None, + fetch_all: bool = False, + enabled_only: bool = False, + tags: list[str] | None = None, + ) -> dict[str, Any]: + query: dict[str, Any] = {} + if enabled_only: + query["status"] = True + if tags: + query["$and"] = [_tag_match_clause(t) for t in tags] + return await self._paginate( + self._materials(), + query, + page=page, + page_size=page_size, + fetch_all=fetch_all, + map_doc=self._map_material, + list_key="materials", + ) + + async def search_audio(self, query_text: str, top_k: int | None = None) -> SearchAudioData: + text = query_text.strip() + if not text: + raise InvalidListParamsError("query_text 不能为空") + if self._retrieval is None: + raise AppError( + message="检索服务未就绪", + status_code=HttpStatus.SERVICE_UNAVAILABLE, + ) + req = SearchAudioRequest(query_text=text, top_k=top_k) + materials = await self._retrieval.search(req) + return SearchAudioData(materials=materials) + + async def _paginate( + self, + collection: AsyncIOMotorCollection, + query: dict[str, Any], + *, + page: int | None, + page_size: int | None, + fetch_all: bool, + map_doc, + list_key: str, + ) -> dict[str, Any]: + total = await collection.count_documents(query) + settings = self._settings + if fetch_all: + if total > settings.fetch_all_hard_limit: + raise InvalidListParamsError( + f"全量条数超过上限 {settings.fetch_all_hard_limit}" + ) + cursor = collection.find(query) + docs = [map_doc(bson_to_jsonable(d)) async for d in cursor] + return { + list_key: docs, + "page": { + "page": 1, + "page_size": len(docs), + "total": total, + "total_pages": 1, + }, + } + cur_page = page or 1 + size = page_size or settings.default_page_size + if cur_page < 1 or size < 1: + raise InvalidListParamsError("page / page_size 须 ≥ 1") + size = min(size, settings.max_page_size) + skip = (cur_page - 1) * size + cursor = collection.find(query).skip(skip).limit(size) + docs = [map_doc(bson_to_jsonable(d)) async for d in cursor] + total_pages = math.ceil(total / size) if size else 0 + return { + list_key: docs, + "page": { + "page": cur_page, + "page_size": size, + "total": total, + "total_pages": total_pages, + }, + } + + def _map_tag(self, doc: dict[str, Any]) -> dict[str, Any]: + name = str(doc.get("name") or "") + name_en = str(doc.get("name_en") or "") + display = name or name_en + parent_id = str(doc.get("parent_tag_id") or "") + return { + "id": str(doc.get("id") or doc.get("_id") or ""), + "display_name": display, + "name": name, + "name_en": name_en, + "type": str(doc.get("type") or ""), + "code": str(doc.get("code") or ""), + "status": str(doc.get("status") or ""), + "parent_tag_id": parent_id, + "parent_tag_name": str(doc.get("parent_tag_name") or ""), + "created_at": str(doc.get("created_at") or ""), + "updated_at": str(doc.get("updated_at") or ""), + } + + def _map_material(self, doc: dict[str, Any]) -> dict[str, Any]: + mid = str(doc.get("id") or doc.get("_id") or "") + return { + "id": mid, + "audio_name": str(doc.get("audio_name") or ""), + "audio_url": str(doc.get("audio_url") or ""), + "cover_url": str(doc.get("cover_url") or ""), + "description": str(doc.get("description") or ""), + "status": bool(doc.get("status", False)), + "operation_type": int(doc.get("operation_type") or 0), + "created_by": str(doc.get("created_by") or ""), + "updated_by": str(doc.get("updated_by") or ""), + "create_time": str(doc.get("create_time") or doc.get("created_at") or ""), + "update_time": str(doc.get("update_time") or doc.get("updated_at") or ""), + "sleep_stage_tags": doc.get("sleep_stage_tags") or [], + "content_form_tags": doc.get("content_form_tags") or [], + "mechanism_tags": doc.get("mechanism_tags") or [], + "audio_engineering_tags": doc.get("audio_engineering_tags") or [], + "medical_risk_tags": doc.get("medical_risk_tags") or [], + "evidence_level_tags": doc.get("evidence_level_tags") or [], + } + + +def _tag_match_clause(tag: str) -> dict[str, Any]: + """素材各维标签 name/code 任一匹配。""" + fields = ( + "sleep_stage_tags", + "content_form_tags", + "mechanism_tags", + "audio_engineering_tags", + "medical_risk_tags", + "evidence_level_tags", + ) + ors: list[dict[str, Any]] = [] + for field in fields: + ors.append({f"{field}.name": tag}) + ors.append({f"{field}.code": tag}) + return {"$or": ors} diff --git a/app/server/somni/quiz/__init__.py b/app/server/somni/quiz/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/somni/quiz/rpc.py b/app/server/somni/quiz/rpc.py new file mode 100644 index 0000000..713c77f --- /dev/null +++ b/app/server/somni/quiz/rpc.py @@ -0,0 +1,51 @@ +"""量产问卷 gRPC 适配。""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, Any + +from app.core.exceptions import ServiceNotReadyError +from app.server.errors import abort_from_app_error, abort_invalid, run_rpc_call +from app.uburnode_grpc.grpc_gen import uburnode_somni_pb2, uburnode_somni_pb2_grpc + +if TYPE_CHECKING: + from app.server.somni.quiz.service import QuizService + + +class QuizRpc(uburnode_somni_pb2_grpc.QuizServiceServicer): + def __init__(self, service: QuizService | None) -> None: + self._service = service + + async def GetAnswer(self, request, context): + if not request.uid.strip() or not request.answer_id.strip(): + await abort_invalid(context, "uid 与 answer_id 均不能为空") + service = await self._require(context) + + async def _do(): + payload = await service.get_answer(request.uid, request.answer_id) + return _to_res(payload) + + return await run_rpc_call(context, _do) + + async def _require(self, context) -> QuizService: + if self._service is None: + await abort_from_app_error(context, ServiceNotReadyError()) + return self._service # type: ignore[return-value] + + +def _to_res(payload: dict[str, Any]) -> uburnode_somni_pb2.GetAnswerRes: + items = [ + { + "question_id": str(item.get("question_id") or ""), + "input_type": str(item.get("input_type") or ""), + "title": str(item.get("title") or ""), + "value": item.get("value"), + "extra_input": str(item.get("extra_input") or ""), + } + for item in (payload.get("answers") or []) + if isinstance(item, dict) + ] + return uburnode_somni_pb2.GetAnswerRes( + answers=json.dumps(items, ensure_ascii=False, separators=(",", ":")), + ) diff --git a/app/server/somni/quiz/service.py b/app/server/somni/quiz/service.py new file mode 100644 index 0000000..31a97ab --- /dev/null +++ b/app/server/somni/quiz/service.py @@ -0,0 +1,58 @@ +"""量产问卷:按 uid + answer_id 查 somni_quiz_answers。""" + +from __future__ import annotations + +from typing import Any + +from bson import ObjectId +from bson.errors import InvalidId +from motor.motor_asyncio import AsyncIOMotorClient + +from app.core.bson_util import bson_to_jsonable +from app.core.codes import HttpStatus +from app.core.config import Settings +from app.core.exceptions import AppError, MongoNotConfiguredError + + +class QuizService: + def __init__(self, client: AsyncIOMotorClient | None, settings: Settings) -> None: + self._client = client + self._settings = settings + + async def get_answer(self, uid: str, answer_id: str) -> dict[str, Any]: + if self._client is None: + raise MongoNotConfiguredError() + collection = self._client[self._settings.somni_mongo_db][ + self._settings.somni_mongo_answers_collection + ] + doc = await collection.find_one({"uid": uid, **_id_query(answer_id)}) + if doc is None: + raise AppError( + message=f"答卷不存在:{answer_id}", + status_code=HttpStatus.NOT_FOUND, + ) + raw = bson_to_jsonable(doc) + answers = [_normalize_answer(item) for item in (raw.get("answers") or [])] + return {"answers": answers} + + +def _id_query(answer_id: str) -> dict[str, Any]: + try: + return {"_id": ObjectId(answer_id)} + except InvalidId: + return {"$or": [{"_id": answer_id}, {"id": answer_id}]} + + +def _normalize_answer(item: Any) -> dict[str, Any]: + if not isinstance(item, dict): + raise AppError( + message="答卷明细格式非法", + status_code=HttpStatus.INTERNAL_SERVER_ERROR, + ) + return { + "question_id": str(item.get("question_id") or ""), + "input_type": str(item.get("input_type") or ""), + "title": str(item.get("title") or ""), + "value": item.get("value"), + "extra_input": str(item.get("extra_input") or ""), + } diff --git a/app/server/somni/report/__init__.py b/app/server/somni/report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/somni/report/calc.py b/app/server/somni/report/calc.py new file mode 100644 index 0000000..2141958 --- /dev/null +++ b/app/server/somni/report/calc.py @@ -0,0 +1,114 @@ +"""量产报告纯计算:本地日窗口、卧床/阶段分钟、均值 floor。""" + +from __future__ import annotations + +import math +from collections.abc import Iterable +from datetime import UTC, date, datetime, timedelta +from typing import Any +from zoneinfo import ZoneInfo + +REPORT_TZ = ZoneInfo("Asia/Shanghai") + + +def parse_record_date(record_date: str) -> date: + return date.fromisoformat(record_date) + + +def local_day_utc_range(record_date: str) -> tuple[datetime, datetime]: + """record_date 本地日 [00:00, 次日 00:00) 转 UTC 感知时间。""" + day = parse_record_date(record_date) + start_local = datetime(day.year, day.month, day.day, tzinfo=REPORT_TZ) + end_local = start_local + timedelta(days=1) + return start_local.astimezone(UTC), end_local.astimezone(UTC) + + +def ensure_aware(value: datetime) -> datetime: + if value.tzinfo is None: + return value.replace(tzinfo=UTC) + return value + + +def to_report_tz(value: datetime) -> datetime: + return ensure_aware(value).astimezone(REPORT_TZ) + + +def minutes_between(start: datetime | None, end: datetime | None) -> int: + if start is None or end is None: + return 0 + delta = to_report_tz(end) - to_report_tz(start) + return max(0, math.floor(delta.total_seconds() / 60)) + + +def stage_minutes(bed_minutes: int, ratio: int | float | None) -> int: + if bed_minutes <= 0 or ratio is None: + return 0 + return math.floor(bed_minutes * float(ratio) / 100) + + +def as_int(value: Any, default: int = 0) -> int: + if value is None: + return default + try: + return int(value) + except (TypeError, ValueError): + return default + + +def as_float(value: Any) -> float | None: + if value is None: + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + +def floor_avg(values: Iterable[float]) -> int: + nums = list(values) + if not nums: + return 0 + return math.floor(sum(nums) / len(nums)) + + +def floor_metric_stats(values: Iterable[float]) -> dict[str, int]: + nums = list(values) + if not nums: + return {"value": 0, "min": 0, "max": 0} + return { + "value": math.floor(sum(nums) / len(nums)), + "min": math.floor(min(nums)), + "max": math.floor(max(nums)), + } + + +def format_hhmm(value: datetime | None) -> str: + if value is None: + return "" + return to_report_tz(value).strftime("%H:%M") + + +def format_collected_at(value: datetime | None) -> str: + if value is None: + return "" + return to_report_tz(value).isoformat() + + +def sleep_stage_parts(raw: dict[str, Any]) -> dict[str, Any]: + bed = minutes_between(raw.get("bed_time"), raw.get("wake_up_time")) + awake_ratio = as_int(raw.get("awake_ratio")) + rem_ratio = as_int(raw.get("rem_ratio")) + light_ratio = as_int(raw.get("light_sleep_ratio")) + deep_ratio = as_int(raw.get("deep_sleep_ratio")) + awake = stage_minutes(bed, awake_ratio) + rem = stage_minutes(bed, rem_ratio) + light = stage_minutes(bed, light_ratio) + deep = stage_minutes(bed, deep_ratio) + return { + "bed_minutes": bed, + "awake": {"minutes": awake, "percent": awake_ratio}, + "rem_sleep": {"minutes": rem, "percent": rem_ratio}, + "light_sleep": {"minutes": light, "percent": light_ratio}, + "deep_sleep": {"minutes": deep, "percent": deep_ratio}, + "total_minutes": deep + light + rem, + } diff --git a/app/server/somni/report/rpc.py b/app/server/somni/report/rpc.py new file mode 100644 index 0000000..fbe5d68 --- /dev/null +++ b/app/server/somni/report/rpc.py @@ -0,0 +1,199 @@ +"""量产睡眠报告 gRPC 适配。""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from app.core.exceptions import ServiceNotReadyError +from app.server.errors import abort_from_app_error, abort_invalid, run_rpc_call +from app.uburnode_grpc.grpc_gen import uburnode_somni_pb2, uburnode_somni_pb2_grpc + +if TYPE_CHECKING: + from app.server.somni.report.service import ReportService + + +class ReportRpc(uburnode_somni_pb2_grpc.ReportServiceServicer): + def __init__(self, service: ReportService | None) -> None: + self._service = service + + async def GetSummary(self, request, context): + return await self._call(request, context, "get_summary", _to_summary_res) + + async def GetEvents(self, request, context): + return await self._call(request, context, "get_events", _to_events_res) + + async def GetEnvironment(self, request, context): + return await self._call(request, context, "get_environment", _to_environment_res) + + async def GetStructure(self, request, context): + return await self._call(request, context, "get_structure", _to_structure_res) + + async def GetSleepQuality(self, request, context): + return await self._call( + request, context, "get_sleep_quality", _to_sleep_quality_res + ) + + async def _call(self, request, context, method_name: str, to_res): + if not request.uid.strip() or not request.record_date.strip(): + await abort_invalid(context, "uid 与 record_date 均不能为空") + service = await self._require(context) + + async def _do(): + payload = await getattr(service, method_name)( + request.uid, request.record_date + ) + return to_res(payload) + + return await run_rpc_call(context, _do) + + async def _require(self, context) -> ReportService: + if self._service is None: + await abort_from_app_error(context, ServiceNotReadyError()) + return self._service # type: ignore[return-value] + + +def _to_summary_res(payload: dict[str, Any]) -> uburnode_somni_pb2.GetSummaryRes: + item = payload.get("sleep_summary") or {} + return uburnode_somni_pb2.GetSummaryRes( + sleep_summary=uburnode_somni_pb2.SleepSummary( + body_battery=int(item.get("body_battery") or 0), + body_battery_status=str(item.get("body_battery_status") or ""), + total_minutes=int(item.get("total_minutes") or 0), + deep_sleep_minutes=int(item.get("deep_sleep_minutes") or 0), + avg_heart_rate=int(item.get("avg_heart_rate") or 0), + avg_respiratory_rate=int(item.get("avg_respiratory_rate") or 0), + ) + ) + + +def _to_environment_res( + payload: dict[str, Any], +) -> uburnode_somni_pb2.GetEnvironmentRes: + summary = payload.get("environment_summary") or {} + return uburnode_somni_pb2.GetEnvironmentRes( + environment_summary=uburnode_somni_pb2.EnvironmentSummary( + temperature=_env_metric(summary.get("temperature")), + humidity=_env_metric(summary.get("humidity")), + illuminance=_env_metric(summary.get("illuminance")), + noise=_env_metric(summary.get("noise")), + ) + ) + + +def _env_metric(item: Any) -> uburnode_somni_pb2.EnvMetric: + data = item if isinstance(item, dict) else {} + return uburnode_somni_pb2.EnvMetric( + value=int(data.get("value") or 0), + min=int(data.get("min") or 0), + max=int(data.get("max") or 0), + ) + + +def _to_structure_res(payload: dict[str, Any]) -> uburnode_somni_pb2.GetStructureRes: + structure = payload.get("sleep_structure") or {} + return uburnode_somni_pb2.GetStructureRes( + sleep_structure=uburnode_somni_pb2.SleepStructure( + awake=_stage_part(structure.get("awake")), + rem_sleep=_stage_part(structure.get("rem_sleep")), + light_sleep=_stage_part(structure.get("light_sleep")), + deep_sleep=_stage_part(structure.get("deep_sleep")), + ) + ) + + +def _stage_part(item: Any) -> uburnode_somni_pb2.SleepStagePart: + data = item if isinstance(item, dict) else {} + return uburnode_somni_pb2.SleepStagePart( + minutes=int(data.get("minutes") or 0), + percent=int(data.get("percent") or 0), + ) + + +def _to_sleep_quality_res( + payload: dict[str, Any], +) -> uburnode_somni_pb2.GetSleepQualityRes: + item = payload.get("sleep_quality") or {} + return uburnode_somni_pb2.GetSleepQualityRes( + sleep_quality=uburnode_somni_pb2.SleepQuality( + time_in_bed_minutes=int(item.get("time_in_bed_minutes") or 0), + sleep_onset_latency_minutes=int( + item.get("sleep_onset_latency_minutes") or 0 + ), + sleep_efficiency=int(item.get("sleep_efficiency") or 0), + bedtime=str(item.get("bedtime") or ""), + wake_up_time=str(item.get("wake_up_time") or ""), + awake_after_onset_minutes=int(item.get("awake_after_onset_minutes") or 0), + ) + ) + + +def _to_events_res(payload: dict[str, Any]) -> uburnode_somni_pb2.GetEventsRes: + res = uburnode_somni_pb2.GetEventsRes( + record_date=str(payload.get("record_date") or ""), + event_count=int(payload.get("event_count") or 0), + abnormal_count=int(payload.get("abnormal_count") or 0), + intervention_count=int(payload.get("intervention_count") or 0), + ) + for item in payload.get("sleep_events") or []: + res.sleep_events.append(_sleep_event_item(item)) + for item in payload.get("idf_data") or []: + res.idf_data.append( + uburnode_somni_pb2.IdfStage( + stage=str(item.get("stage") or ""), + start=str(item.get("start") or ""), + end=str(item.get("end") or ""), + ) + ) + for item in payload.get("physio_data") or []: + metrics = item.get("metrics") or {} + res.physio_data.append( + uburnode_somni_pb2.PhysioDataPoint( + collected_at=str(item.get("collected_at") or ""), + metrics=uburnode_somni_pb2.PhysioMetrics( + heart_rate=int(metrics.get("heart_rate") or 0), + respiration_rate=int(metrics.get("respiration_rate") or 0), + ), + ) + ) + for item in payload.get("env_data") or []: + res.env_data.append( + uburnode_somni_pb2.EnvDataPoint( + collected_at=str(item.get("collected_at") or ""), + temperature=int(item.get("temperature") or 0), + humidity=int(item.get("humidity") or 0), + illuminance=int(item.get("illuminance") or 0), + noise=int(item.get("noise") or 0), + ) + ) + return res + + +def _sleep_event_item(item: dict[str, Any]) -> uburnode_somni_pb2.SleepEventItem: + event = uburnode_somni_pb2.SleepEventItem( + event_time=str(item.get("event_time") or ""), + type=str(item.get("type") or ""), + code=str(item.get("code") or ""), + ) + for detail in item.get("events") or []: + event.events.append( + uburnode_somni_pb2.SleepEventDetail( + event_type=str(detail.get("event_type") or ""), + duration=str(detail.get("duration") or ""), + trigger_cause=str(detail.get("trigger_cause") or ""), + action_taken=str(detail.get("action_taken") or ""), + result_summary=str(detail.get("result_summary") or ""), + ) + ) + intervention = item.get("intervention") or {} + event.intervention.CopyFrom( + uburnode_somni_pb2.Intervention( + type=str(intervention.get("type") or ""), + event_time=str(intervention.get("event_time") or ""), + event_type=str(intervention.get("event_type") or ""), + duration=str(intervention.get("duration") or ""), + trigger_cause=str(intervention.get("trigger_cause") or ""), + action_taken=str(intervention.get("action_taken") or ""), + result_summary=str(intervention.get("result_summary") or ""), + ) + ) + return event diff --git a/app/server/somni/report/service.py b/app/server/somni/report/service.py new file mode 100644 index 0000000..6aa53eb --- /dev/null +++ b/app/server/somni/report/service.py @@ -0,0 +1,256 @@ +"""量产睡眠报告:按 uid + record_date 聚合。""" + +from __future__ import annotations + +from typing import Any + +from loguru import logger +from motor.motor_asyncio import AsyncIOMotorClient + +from app.core.config import Settings +from app.server.somni.report import calc +from app.server.somni.report.store import ReportStore + +_INTERVENTION_FIELDS = ( + "type", + "event_time", + "event_type", + "duration", + "trigger_cause", + "action_taken", + "result_summary", +) + + +class ReportService: + def __init__( + self, + client: AsyncIOMotorClient | None, + settings: Settings, + store: ReportStore | None = None, + ) -> None: + self._store = store or ReportStore(client, settings) + + async def get_summary(self, uid: str, record_date: str) -> dict[str, Any]: + record = await self._store.find_record(uid, record_date) + report = await self._store.find_sleep_report(uid, record_date) + parts = calc.sleep_stage_parts(_raw_data(record)) + hr_vals, br_vals = await self._sleep_hr_br(uid, record_date) + summary = (report or {}).get("sleep_summary") or {} + return { + "sleep_summary": { + "body_battery": calc.as_int(summary.get("body_battery")), + "body_battery_status": str(summary.get("body_battery_status") or ""), + "total_minutes": int(parts["total_minutes"]), + "deep_sleep_minutes": int(parts["deep_sleep"]["minutes"]), + "avg_heart_rate": calc.floor_avg(hr_vals), + "avg_respiratory_rate": calc.floor_avg(br_vals), + } + } + + async def get_environment(self, uid: str, record_date: str) -> dict[str, Any]: + docs = await self._telemetry(uid, record_date, "env") + return { + "environment_summary": { + "temperature": calc.floor_metric_stats(_data_floats(docs, "temp")), + "humidity": calc.floor_metric_stats(_data_floats(docs, "humi")), + "illuminance": calc.floor_metric_stats(_data_floats(docs, "lux")), + "noise": calc.floor_metric_stats(_data_floats(docs, "noise_db")), + } + } + + async def get_events(self, uid: str, record_date: str) -> dict[str, Any]: + events = await self._store.list_events(uid, record_date) + sleep_events, abnormal_count, intervention_count = _assemble_sleep_events(events) + record = await self._store.find_record(uid, record_date) + sleep_docs = await self._telemetry(uid, record_date, "sleep") + env_docs = await self._telemetry(uid, record_date, "env") + return { + "record_date": record_date, + "sleep_events": sleep_events, + "event_count": abnormal_count, + "abnormal_count": abnormal_count, + "intervention_count": intervention_count, + "idf_data": _idf_data(record), + "physio_data": _physio_data(sleep_docs), + "env_data": _env_data(env_docs), + } + + async def get_structure(self, uid: str, record_date: str) -> dict[str, Any]: + record = await self._store.find_record(uid, record_date) + parts = calc.sleep_stage_parts(_raw_data(record)) + return { + "sleep_structure": { + "awake": parts["awake"], + "rem_sleep": parts["rem_sleep"], + "light_sleep": parts["light_sleep"], + "deep_sleep": parts["deep_sleep"], + } + } + + async def get_sleep_quality(self, uid: str, record_date: str) -> dict[str, Any]: + record = await self._store.find_record(uid, record_date) + raw = _raw_data(record) + bed = raw.get("bed_time") + wake = raw.get("wake_time") + wake_up = raw.get("wake_up_time") + return { + "sleep_quality": { + "time_in_bed_minutes": calc.minutes_between(bed, wake_up), + "sleep_onset_latency_minutes": calc.as_int(raw.get("sleep_latency")), + "sleep_efficiency": calc.as_int(raw.get("sleep_efficiency")), + "bedtime": calc.format_hhmm(bed), + "wake_up_time": calc.format_hhmm(wake_up), + "awake_after_onset_minutes": calc.minutes_between(wake, wake_up), + } + } + + async def _telemetry( + self, uid: str, record_date: str, metric: str + ) -> list[dict[str, Any]]: + device_id = await self._store.find_device_id(uid) + if not device_id: + return [] + return await self._store.list_telemetry(device_id, metric, record_date) + + async def _sleep_hr_br( + self, uid: str, record_date: str + ) -> tuple[list[float], list[float]]: + docs = await self._telemetry(uid, record_date, "sleep") + return _data_floats(docs, "hr"), _data_floats(docs, "br") + + +def _raw_data(record: dict[str, Any] | None) -> dict[str, Any]: + if not record: + return {} + raw = record.get("raw_data") or {} + return raw if isinstance(raw, dict) else {} + + +def _data_floats(docs: list[dict[str, Any]], key: str) -> list[float]: + values: list[float] = [] + for doc in docs: + data = doc.get("data") or {} + if not isinstance(data, dict): + continue + num = calc.as_float(data.get(key)) + if num is not None: + values.append(num) + return values + + +def _idf_data(record: dict[str, Any] | None) -> list[dict[str, str]]: + if not record: + return [] + items = record.get("idf_data") or [] + result: list[dict[str, str]] = [] + for item in items: + if not isinstance(item, dict): + continue + result.append( + { + "stage": str(item.get("stage") or ""), + "start": str(item.get("start") or ""), + "end": str(item.get("end") or ""), + } + ) + return result + + +def _physio_data(docs: list[dict[str, Any]]) -> list[dict[str, Any]]: + points: list[dict[str, Any]] = [] + for doc in docs: + data = doc.get("data") or {} + hr = calc.as_float(data.get("hr") if isinstance(data, dict) else None) + br = calc.as_float(data.get("br") if isinstance(data, dict) else None) + points.append( + { + "collected_at": calc.format_collected_at(doc.get("ts")), + "metrics": { + "heart_rate": 0 if hr is None else calc.floor_avg([hr]), + "respiration_rate": 0 if br is None else calc.floor_avg([br]), + }, + } + ) + return points + + +def _env_data(docs: list[dict[str, Any]]) -> list[dict[str, Any]]: + points: list[dict[str, Any]] = [] + for doc in docs: + data = doc.get("data") if isinstance(doc.get("data"), dict) else {} + points.append( + { + "collected_at": calc.format_collected_at(doc.get("ts")), + "temperature": _floor_one(data.get("temp")), + "humidity": _floor_one(data.get("humi")), + "illuminance": _floor_one(data.get("lux")), + "noise": _floor_one(data.get("noise_db")), + } + ) + return points + + +def _floor_one(value: Any) -> int: + num = calc.as_float(value) + return 0 if num is None else calc.floor_avg([num]) + + +def _assemble_sleep_events( + events: list[dict[str, Any]], +) -> tuple[list[dict[str, Any]], int, int]: + abnormals: list[dict[str, Any]] = [] + interventions: list[dict[str, Any]] = [] + for doc in events: + event_type = str(doc.get("type") or "") + if event_type == "abnormal": + abnormals.append(doc) + elif event_type == "intervention": + interventions.append(doc) + + by_id = {_event_id(doc): _to_sleep_event_item(doc) for doc in abnormals} + used_parents: set[str] = set() + for doc in interventions: + parent_id = str(doc.get("related_event_id") or "") + parent = by_id.get(parent_id) + if parent is None: + logger.debug("intervention 找不到父级 related_event_id={}", parent_id) + continue + if parent_id in used_parents: + logger.debug("父级已有 intervention,忽略多余条 parent_id={}", parent_id) + continue + parent["intervention"] = _to_intervention(doc) + used_parents.add(parent_id) + + return list(by_id.values()), len(abnormals), len(interventions) + + +def _event_id(doc: dict[str, Any]) -> str: + return str(doc.get("_id") or doc.get("id") or "") + + +def _to_sleep_event_item(doc: dict[str, Any]) -> dict[str, Any]: + details = [] + for item in doc.get("events") or []: + if not isinstance(item, dict): + continue + details.append( + { + "event_type": str(item.get("event_type") or ""), + "duration": str(item.get("duration") or ""), + "trigger_cause": str(item.get("trigger_cause") or ""), + "action_taken": str(item.get("action_taken") or ""), + "result_summary": str(item.get("result_summary") or ""), + } + ) + return { + "event_time": str(doc.get("event_time") or ""), + "type": str(doc.get("type") or ""), + "code": str(doc.get("code") or ""), + "events": details, + "intervention": {field: "" for field in _INTERVENTION_FIELDS}, + } + + +def _to_intervention(doc: dict[str, Any]) -> dict[str, str]: + return {field: str(doc.get(field) or "") for field in _INTERVENTION_FIELDS} diff --git a/app/server/somni/report/store.py b/app/server/somni/report/store.py new file mode 100644 index 0000000..5f7f3ba --- /dev/null +++ b/app/server/somni/report/store.py @@ -0,0 +1,71 @@ +"""量产报告 Mongo 查询。""" + +from __future__ import annotations + +from typing import Any + +from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase + +from app.core.config import Settings +from app.core.exceptions import MongoNotConfiguredError +from app.server.somni.report.calc import local_day_utc_range + + +class ReportStore: + def __init__(self, client: AsyncIOMotorClient | None, settings: Settings) -> None: + self._client = client + self._settings = settings + + def _db(self) -> AsyncIOMotorDatabase: + if self._client is None: + raise MongoNotConfiguredError() + return self._client[self._settings.somni_mongo_db] + + async def find_device_id(self, uid: str) -> str | None: + doc = await self._db()[self._settings.somni_mongo_devices_collection].find_one( + {"bind_uid": uid}, + {"device_id": 1}, + ) + if not doc: + return None + device_id = doc.get("device_id") + return str(device_id) if device_id else None + + async def list_telemetry( + self, + device_id: str, + metric: str, + record_date: str, + ) -> list[dict[str, Any]]: + start, end = local_day_utc_range(record_date) + cursor = self._db()[self._settings.somni_mongo_telemetry_collection].find( + { + "device_id": device_id, + "metric": metric, + "ts": {"$gte": start, "$lt": end}, + } + ).sort("ts", 1) + return await cursor.to_list(length=10_000) + + async def find_record(self, uid: str, record_date: str) -> dict[str, Any] | None: + return await self._db()[self._settings.somni_mongo_records_collection].find_one( + {"uid": uid, "record_date": record_date} + ) + + async def find_sleep_report( + self, uid: str, record_date: str + ) -> dict[str, Any] | None: + return await self._db()[ + self._settings.somni_mongo_sleep_reports_collection + ].find_one({"uid": uid, "record_date": record_date}) + + async def list_events(self, uid: str, record_date: str) -> list[dict[str, Any]]: + doc = await self._db()[self._settings.somni_mongo_events_collection].find_one( + {"uid": uid, "record_date": record_date} + ) + if not doc: + return [] + nested = doc.get("sleep_events") + if isinstance(nested, list): + return [item for item in nested if isinstance(item, dict)] + return [doc] diff --git a/app/services/retrieval.py b/app/services/retrieval.py index 0cb0431..cbf3c12 100644 --- a/app/services/retrieval.py +++ b/app/services/retrieval.py @@ -17,6 +17,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any +import numpy as np from loguru import logger from app.core.config import Settings @@ -95,6 +96,78 @@ class ExtractedQueryTags: disliked_tags: list[str] +@dataclass(frozen=True) +class VectorSimilaritySnapshot: + """请求向量与候选标签向量的一次性批量余弦结果。""" + + scores: np.ndarray + tag_columns: dict[str, int] + + @classmethod + def build( + cls, + request_vectors: list[list[float]], + dictionary_vectors: DictionaryVectors, + ) -> VectorSimilaritySnapshot: + dictionary_items = [ + (tag_id, vector) for tag_id, vector in dictionary_vectors.items() if vector + ] + tag_columns = {tag_id: column for column, (tag_id, _vector) in enumerate(dictionary_items)} + if not request_vectors or not dictionary_items: + return cls( + scores=np.zeros( + (len(request_vectors), len(dictionary_items)), + dtype=np.float64, + ), + tag_columns=tag_columns, + ) + scores = _cosine_similarity_matrix( + request_vectors, + [vector for _tag_id, vector in dictionary_items], + ) + return cls(scores=scores, tag_columns=tag_columns) + + def count_matches( + self, + tag_ids: list[str], + request_tags: list[str], + *, + row_offset: int, + threshold: float, + ) -> int: + columns = [self.tag_columns[tag_id] for tag_id in tag_ids if tag_id in self.tag_columns] + if not columns or not request_tags: + return 0 + end = row_offset + len(request_tags) + if row_offset < 0 or end > self.scores.shape[0]: + raise ValueError("similarity snapshot row range is invalid") + block = self.scores[row_offset:end, columns] + return sum( + request_tag not in MUTUALLY_EXCLUSIVE_CONTENT_TAGS + and bool(np.any(block[row] >= threshold)) + for row, request_tag in enumerate(request_tags) + ) + + def max_similarity( + self, + tag_ids: list[str], + request_tags: list[str], + *, + row_offset: int, + ) -> float: + columns = [self.tag_columns[tag_id] for tag_id in tag_ids if tag_id in self.tag_columns] + rows = [ + row_offset + row + for row, request_tag in enumerate(request_tags) + if request_tag not in MUTUALLY_EXCLUSIVE_CONTENT_TAGS + ] + if not columns or not rows: + return 0.0 + if rows[-1] >= self.scores.shape[0] or row_offset < 0: + raise ValueError("similarity snapshot row range is invalid") + return float(np.max(self.scores[np.ix_(rows, columns)])) + + class RetrievalService: """三维度检索:睡眠阶段 → 内容形态 → 厌恶剔除 → 粗排 → 精排。""" @@ -147,18 +220,27 @@ async def _search_tag_only(self, request: SearchAudioRequest) -> list[dict[str, candidates_raw, _normalize_color_noise_aliases(request.content_tags), ) - dictionary_vectors, content_tags, disliked_tags, content_vectors, dislike_vectors = ( - await self._prepare_content_admission_inputs( - candidates_raw, - request, - need_dictionary=need_dictionary, - ) + ( + dictionary_vectors, + content_tags, + disliked_tags, + content_vectors, + dislike_vectors, + ) = await self._prepare_content_admission_inputs( + candidates_raw, + request, + need_dictionary=need_dictionary, + ) + similarity_snapshot = VectorSimilaritySnapshot.build( + [*content_vectors, *dislike_vectors], + dictionary_vectors, ) admitted = await self._apply_content_admission( candidates_raw, content_tags, dictionary_vectors, request_vectors=content_vectors, + similarity_snapshot=similarity_snapshot, ) step2_ms = _elapsed_ms(step2_started) logger.info( @@ -178,6 +260,8 @@ async def _search_tag_only(self, request: SearchAudioRequest) -> list[dict[str, vector_disliked, dictionary_vectors, dislike_vectors=dislike_vectors, + similarity_snapshot=similarity_snapshot, + similarity_row_offset=len(content_vectors), ) filtered = _apply_voice_code_filter( filtered, @@ -296,6 +380,11 @@ async def _search_text_multi_route( await asyncio.gather(dict_task, encode_task, return_exceptions=True) raise + similarity_snapshot = VectorSimilaritySnapshot.build( + [*content_vectors, *dislike_vectors], + dictionary_vectors, + ) + tag_candidates: list[ScoredCandidate] = [] if content_tags: tag_candidates = await self._score_content_candidates( @@ -303,6 +392,7 @@ async def _search_text_multi_route( content_tags, dictionary_vectors, request_vectors=content_vectors, + similarity_snapshot=similarity_snapshot, ) merged = await self._merge_and_rank_text_candidates( @@ -313,6 +403,8 @@ async def _search_text_multi_route( voice_filter_tags=disliked_tags, dictionary_vectors=dictionary_vectors, dislike_vectors=dislike_vectors, + similarity_snapshot=similarity_snapshot, + similarity_row_offset=len(content_vectors), top_k=request.top_k, ) rank_ms = _elapsed_ms(rank_started) @@ -360,8 +452,7 @@ async def _fetch_step1_candidates(self, sleep_stage_tags: list[str]) -> list[dic ) return cached - candidates = await self._es_search.filter_by_sleep_stage(sleep_stage_tags) - await self._backfill_sleep_stage_cache() + candidates = await self._load_sleep_stage_candidates_on_miss(sleep_stage_tags) logger.info( "检索步骤1/4 睡眠阶段过滤:候选数={},耗时={:.1f}毫秒", len(candidates), @@ -369,6 +460,22 @@ async def _fetch_step1_candidates(self, sleep_stage_tags: list[str]) -> list[dic ) return candidates + async def _load_sleep_stage_candidates_on_miss( + self, + sleep_stage_tags: list[str], + ) -> list[dict[str, Any]]: + """缓存 miss 时只回填请求阶段;缓存故障则直接回退 ES。""" + if self._sleep_stage_cache is None: + return await self._es_search.filter_by_sleep_stage(sleep_stage_tags) + try: + return await self._sleep_stage_cache.get_or_load( + sleep_stage_tags, + self._load_sleep_stage_candidates, + ) + except Exception as exc: + logger.warning("按需回填睡眠阶段候选缓存失败,回退 ES:{}", exc) + return await self._es_search.filter_by_sleep_stage(sleep_stage_tags) + async def _get_sleep_stage_cached( self, sleep_stage_tags: list[str], @@ -401,11 +508,7 @@ async def warm_query_tag_vectors(self) -> None: started = time.perf_counter() tags = await self._es_search.list_content_tag_vectors() labels = _unique_preserve_order( - [ - label - for tag in tags - if (label := str(tag.get("label", "")).strip()) - ] + [label for tag in tags if (label := str(tag.get("label", "")).strip())] ) if not labels: logger.info("查询标签向量缓存预热跳过:内容词典为空") @@ -604,6 +707,7 @@ async def _apply_content_admission( dictionary_vectors: DictionaryVectors, *, request_vectors: list[list[float]] | None = None, + similarity_snapshot: VectorSimilaritySnapshot | None = None, ) -> list[ScoredCandidate]: """步骤 2:无 content_tags 时跳过准入,保留睡眠阶段候选全集。""" if not content_tags: @@ -622,6 +726,10 @@ async def _apply_content_admission( request_vectors = ( await self._encode_texts(content_tags) if request_vectors is None else request_vectors ) + similarity_snapshot = similarity_snapshot or VectorSimilaritySnapshot.build( + request_vectors, + dictionary_vectors, + ) admitted: list[ScoredCandidate] = [] for doc in candidates: @@ -646,6 +754,7 @@ async def _apply_content_admission( content_tags, request_vectors, dictionary_vectors, + similarity_snapshot=similarity_snapshot, ) if vector_hits > 0: admitted.append( @@ -667,6 +776,7 @@ async def _score_content_candidates( dictionary_vectors: DictionaryVectors, *, request_vectors: list[list[float]] | None = None, + similarity_snapshot: VectorSimilaritySnapshot | None = None, ) -> list[ScoredCandidate]: """文本多路检索里的标签路:产出分数,不决定整体短路。""" if not candidates or not content_tags: @@ -675,6 +785,10 @@ async def _score_content_candidates( request_vectors = ( await self._encode_texts(content_tags) if request_vectors is None else request_vectors ) + similarity_snapshot = similarity_snapshot or VectorSimilaritySnapshot.build( + request_vectors, + dictionary_vectors, + ) scored: list[ScoredCandidate] = [] tag_count = max(len(content_tags), 1) for doc in candidates: @@ -688,6 +802,7 @@ async def _score_content_candidates( content_tags, request_vectors, dictionary_vectors, + similarity_snapshot=similarity_snapshot, ) match_count = len(exact_hits) if exact_hits else vector_hits if match_count <= 0: @@ -713,25 +828,25 @@ def _count_fuzzy_vector_matches( request_tags: list[str], request_vectors: list[list[float]], dictionary_vectors: DictionaryVectors, + *, + similarity_snapshot: VectorSimilaritySnapshot | None = None, + similarity_row_offset: int = 0, ) -> int: """使用请求级向量快照计分;互斥标签只允许精确命中。""" tag_ids = EsSearch.content_tag_ids(tags) if not tag_ids or not request_vectors: return 0 - threshold = self._settings.sim_threshold - matched = 0 - - for request_tag, req_vec in zip(request_tags, request_vectors, strict=True): - if request_tag in MUTUALLY_EXCLUSIVE_CONTENT_TAGS: - continue - for tid in tag_ids: - doc_vec = dictionary_vectors.get(tid) - if doc_vec and _cosine_similarity(req_vec, doc_vec) >= threshold: - matched += 1 - break - - return matched + snapshot = similarity_snapshot or VectorSimilaritySnapshot.build( + request_vectors, + dictionary_vectors, + ) + return snapshot.count_matches( + tag_ids, + request_tags, + row_offset=similarity_row_offset, + threshold=self._settings.sim_threshold, + ) async def _apply_dislike_filter( self, @@ -740,15 +855,19 @@ async def _apply_dislike_filter( dictionary_vectors: DictionaryVectors, *, dislike_vectors: list[list[float]] | None = None, + similarity_snapshot: VectorSimilaritySnapshot | None = None, + similarity_row_offset: int = 0, ) -> list[ScoredCandidate]: """步骤 3 前半:厌恶标签向量 vs 文档内容标签向量,余弦 ≥ SIM_THRESHOLD 则剔除。""" if not disliked_tags: return candidates dislike_vectors = ( - await self._encode_texts(disliked_tags) - if dislike_vectors is None - else dislike_vectors + await self._encode_texts(disliked_tags) if dislike_vectors is None else dislike_vectors + ) + similarity_snapshot = similarity_snapshot or VectorSimilaritySnapshot.build( + dislike_vectors, + dictionary_vectors, ) result: list[ScoredCandidate] = [] for candidate in candidates: @@ -758,6 +877,8 @@ async def _apply_dislike_filter( disliked_tags, dislike_vectors, dictionary_vectors, + similarity_snapshot=similarity_snapshot, + similarity_row_offset=similarity_row_offset, ) > 0 ): @@ -782,6 +903,8 @@ async def _merge_and_rank_text_candidates( dictionary_vectors: DictionaryVectors, top_k: int | None, dislike_vectors: list[list[float]] | None = None, + similarity_snapshot: VectorSimilaritySnapshot | None = None, + similarity_row_offset: int = 0, voice_filter_tags: list[str] | None = None, ) -> list[ScoredCandidate]: merged: dict[str, ScoredCandidate] = {} @@ -800,6 +923,10 @@ async def _merge_and_rank_text_candidates( if disliked_tags and dislike_vectors is None else (dislike_vectors or []) ) + similarity_snapshot = similarity_snapshot or VectorSimilaritySnapshot.build( + dislike_vectors, + dictionary_vectors, + ) ranked: list[ScoredCandidate] = [] for candidate in merged.values(): penalty = self._dislike_penalty( @@ -807,6 +934,8 @@ async def _merge_and_rank_text_candidates( disliked_tags, dislike_vectors, dictionary_vectors, + similarity_snapshot=similarity_snapshot, + similarity_row_offset=similarity_row_offset, ) if penalty >= 1.0: continue @@ -841,11 +970,18 @@ async def _extract_query_tags( exact_content_tags = _unique_preserve_order( [*color_intents, *_match_labels_from_text(positive_text, tag_vectors)] ) + fragment_vectors = ( + await self._encode_texts(negative_fragments) if negative_fragments else [] + ) + vector_labels, similarity_scores = _tag_vector_similarity_scores( + [query_vector, *fragment_vectors], + tag_vectors, + ) content_tags = list(exact_content_tags) content_tags.extend( - _similar_labels_from_vector( - query_vector, - tag_vectors, + _similar_labels_from_scores( + similarity_scores[0] if similarity_scores.shape[0] else np.zeros(0), + vector_labels, threshold=AUTO_TAG_SIM_THRESHOLD, exclude=set(content_tags), limit=AUTO_TAG_TOP_K - len(content_tags), @@ -858,13 +994,12 @@ async def _extract_query_tags( exact_disliked_tags = _match_labels_from_text(" ".join(negative_fragments), tag_vectors) disliked_tags = list(exact_disliked_tags) - if negative_fragments: - fragment_vectors = await self._encode_texts(negative_fragments) - for vector in fragment_vectors: + if fragment_vectors: + for row in range(1, len(fragment_vectors) + 1): disliked_tags.extend( - _similar_labels_from_vector( - vector, - tag_vectors, + _similar_labels_from_scores( + similarity_scores[row], + vector_labels, threshold=AUTO_DISLIKE_SIM_THRESHOLD, exclude=set(disliked_tags), limit=AUTO_TAG_TOP_K - len(disliked_tags), @@ -890,6 +1025,9 @@ def _dislike_penalty( disliked_tags: list[str], dislike_vectors: list[list[float]], dictionary_vectors: DictionaryVectors, + *, + similarity_snapshot: VectorSimilaritySnapshot | None = None, + similarity_row_offset: int = 0, ) -> float: if not disliked_tags: return 0.0 @@ -902,6 +1040,8 @@ def _dislike_penalty( disliked_tags, dislike_vectors, dictionary_vectors, + similarity_snapshot=similarity_snapshot, + similarity_row_offset=similarity_row_offset, ) if max_similarity >= self._settings.strong_dislike_sim_threshold: return 1.0 @@ -915,20 +1055,23 @@ def _max_fuzzy_vector_similarity( request_tags: list[str], request_vectors: list[list[float]], dictionary_vectors: DictionaryVectors, + *, + similarity_snapshot: VectorSimilaritySnapshot | None = None, + similarity_row_offset: int = 0, ) -> float: tag_ids = EsSearch.content_tag_ids(tags) if not tag_ids or not request_vectors: return 0.0 - max_similarity = 0.0 - for request_tag, req_vec in zip(request_tags, request_vectors, strict=True): - if request_tag in MUTUALLY_EXCLUSIVE_CONTENT_TAGS: - continue - for tag_id in tag_ids: - doc_vec = dictionary_vectors.get(tag_id) - if doc_vec: - max_similarity = max(max_similarity, _cosine_similarity(req_vec, doc_vec)) - return max_similarity + snapshot = similarity_snapshot or VectorSimilaritySnapshot.build( + request_vectors, + dictionary_vectors, + ) + return snapshot.max_similarity( + tag_ids, + request_tags, + row_offset=similarity_row_offset, + ) def _candidate_from_doc( self, @@ -1020,6 +1163,41 @@ def _cosine_similarity(a: list[float], b: list[float]) -> float: return dot / (norm_a * norm_b) +def _cosine_similarity_matrix( + request_vectors: list[list[float]], + dictionary_vectors: list[list[float]], +) -> np.ndarray: + """批量计算二维余弦矩阵,行对应请求向量、列对应词典向量。""" + try: + requests = np.asarray(request_vectors, dtype=np.float64) + dictionary = np.asarray(dictionary_vectors, dtype=np.float64) + except (TypeError, ValueError) as exc: + raise ValueError("similarity vectors must form rectangular numeric matrices") from exc + if requests.ndim != 2 or dictionary.ndim != 2: + raise ValueError("similarity vectors must be two-dimensional") + if requests.shape[1] != dictionary.shape[1]: + raise ValueError( + "similarity vector dimension mismatch: " + f"request={requests.shape[1]}, dictionary={dictionary.shape[1]}" + ) + + request_norms = np.linalg.norm(requests, axis=1, keepdims=True) + dictionary_norms = np.linalg.norm(dictionary, axis=1, keepdims=True) + normalized_requests = np.divide( + requests, + request_norms, + out=np.zeros_like(requests), + where=request_norms != 0, + ) + normalized_dictionary = np.divide( + dictionary, + dictionary_norms, + out=np.zeros_like(dictionary), + where=dictionary_norms != 0, + ) + return normalized_requests @ normalized_dictionary.T + + def _candidate_key(source: dict[str, Any]) -> str: return str( source.get("_id") or source.get("id") or source.get("audio_url") or source.get("audio_name") @@ -1185,9 +1363,7 @@ def _normalize_to_dictionary_labels( ] dictionary = { - str(item["label"]).strip() - for item in tag_vectors - if str(item.get("label", "")).strip() + str(item["label"]).strip() for item in tag_vectors if str(item.get("label", "")).strip() } normalized: list[str] = [] for raw in raw_tags: @@ -1212,23 +1388,62 @@ def _similar_labels_from_vector( threshold: float, exclude: set[str], limit: int, +) -> list[str]: + labels, scores = _tag_vector_similarity_scores([query_vector], tag_vectors) + row = scores[0] if scores.shape[0] else np.zeros(0) + return _similar_labels_from_scores( + row, + labels, + threshold=threshold, + exclude=exclude, + limit=limit, + ) + + +def _tag_vector_similarity_scores( + query_vectors: list[list[float]], + tag_vectors: list[dict[str, Any]], +) -> tuple[list[str], np.ndarray]: + """过滤无效词典项并一次计算全部查询与标签的余弦分数。""" + eligible: list[tuple[str, list[float]]] = [] + for item in tag_vectors: + label = str(item.get("label", "")).strip() + vector = item.get("vector") + if not label or len(label) < MIN_AUTO_TAG_LABEL_LEN or not vector: + continue + eligible.append((label, vector)) + if not query_vectors or not eligible: + return ( + [label for label, _vector in eligible], + np.zeros((len(query_vectors), len(eligible)), dtype=np.float64), + ) + return ( + [label for label, _vector in eligible], + _cosine_similarity_matrix( + query_vectors, + [vector for _label, vector in eligible], + ), + ) + + +def _similar_labels_from_scores( + scores: np.ndarray, + labels: list[str], + *, + threshold: float, + exclude: set[str], + limit: int, ) -> list[str]: if limit <= 0: return [] + if scores.ndim != 1 or scores.shape[0] != len(labels): + raise ValueError("tag similarity scores and labels must have matching lengths") scored: list[tuple[float, str]] = [] - for item in tag_vectors: - label = str(item["label"]).strip() - vector = item.get("vector") - if ( - not label - or len(label) < MIN_AUTO_TAG_LABEL_LEN - or label in exclude - or not vector - ): + for score, label in zip(scores, labels, strict=True): + if label in exclude: continue - similarity = _cosine_similarity(query_vector, vector) - if similarity >= threshold: - scored.append((similarity, label)) + if score >= threshold: + scored.append((float(score), label)) scored.sort(key=lambda pair: pair[0], reverse=True) return _prefer_longer_labels([label for _, label in scored])[:limit] diff --git a/app/uburnode_grpc/__init__.py b/app/uburnode_grpc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/uburnode_grpc/grpc_gen/__init__.py b/app/uburnode_grpc/grpc_gen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/uburnode_grpc/grpc_gen/uburnode_pb2.py b/app/uburnode_grpc/grpc_gen/uburnode_pb2.py new file mode 100644 index 0000000..4490fdb --- /dev/null +++ b/app/uburnode_grpc/grpc_gen/uburnode_pb2.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: uburnode.proto +# Protobuf Python Version: 6.33.5 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 5, + '', + 'uburnode.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0euburnode.proto\x12\x0buburnode.v1\x1a\x1cgoogle/protobuf/struct.proto\"\x17\n\tIdRequest\x12\n\n\x02id\x18\x01 \x01(\t\",\n\x11OperationResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0b\n\x03msg\x18\x02 \x01(\t\"e\n\x0bSomniTagRef\x12\x13\n\x06tag_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x11\n\x04\x63ode\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x11\n\x04name\x18\x03 \x01(\tH\x02\x88\x01\x01\x42\t\n\x07_tag_idB\x07\n\x05_codeB\x07\n\x05_name\"\xea\x01\n\x0e\x43ontentFormTag\x12\x13\n\x06tag_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x11\n\x04\x63ode\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x11\n\x04name\x18\x03 \x01(\tH\x02\x88\x01\x01\x12\x14\n\x07\x65n_name\x18\x04 \x01(\tH\x03\x88\x01\x01\x12\x1a\n\rparent_tag_id\x18\x05 \x01(\tH\x04\x88\x01\x01\x12\x1c\n\x0fparent_tag_code\x18\x06 \x01(\tH\x05\x88\x01\x01\x42\t\n\x07_tag_idB\x07\n\x05_codeB\x07\n\x05_nameB\n\n\x08_en_nameB\x10\n\x0e_parent_tag_idB\x12\n\x10_parent_tag_code\"\xf0\x01\n\x13\x41udioEngineeringTag\x12\x13\n\x06tag_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x11\n\x04\x63ode\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x11\n\x04name\x18\x03 \x01(\tH\x02\x88\x01\x01\x12,\n\x05value\x18\x04 \x01(\x0b\x32\x18.uburnode.v1.SomniTagRefH\x03\x88\x01\x01\x12\x13\n\x0b\x62\x61nd_values\x18\x05 \x03(\x01\x12\x1e\n\x11relative_loudness\x18\x06 \x01(\x01H\x04\x88\x01\x01\x42\t\n\x07_tag_idB\x07\n\x05_codeB\x07\n\x05_nameB\x08\n\x06_valueB\x14\n\x12_relative_loudness\"\xf3\x04\n\x0e\x43reateAudioReq\x12\x12\n\naudio_name\x18\x01 \x01(\t\x12\x16\n\taudio_url\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x1b\n\x0eoperation_type\x18\x04 \x01(\x05H\x02\x88\x01\x01\x12\x17\n\ncreated_by\x18\x05 \x01(\tH\x03\x88\x01\x01\x12\x17\n\nupdated_by\x18\x06 \x01(\tH\x04\x88\x01\x01\x12\x13\n\x06status\x18\x07 \x01(\x08H\x05\x88\x01\x01\x12\x32\n\x10sleep_stage_tags\x18\x08 \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12\x36\n\x11\x63ontent_form_tags\x18\t \x03(\x0b\x32\x1b.uburnode.v1.ContentFormTag\x12\x30\n\x0emechanism_tags\x18\n \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12@\n\x16\x61udio_engineering_tags\x18\x0b \x03(\x0b\x32 .uburnode.v1.AudioEngineeringTag\x12\x33\n\x11medical_risk_tags\x18\x0c \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12\x35\n\x13\x65vidence_level_tags\x18\r \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12\x11\n\tembedding\x18\x0e \x03(\x01\x42\x0c\n\n_audio_urlB\x0e\n\x0c_descriptionB\x11\n\x0f_operation_typeB\r\n\x0b_created_byB\r\n\x0b_updated_byB\t\n\x07_status\"\x9c\x05\n\x0eUpdateAudioReq\x12\x13\n\x0bmaterial_id\x18\x01 \x01(\t\x12\x17\n\naudio_name\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x16\n\taudio_url\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x02\x88\x01\x01\x12\x1b\n\x0eoperation_type\x18\x05 \x01(\x05H\x03\x88\x01\x01\x12\x17\n\ncreated_by\x18\x06 \x01(\tH\x04\x88\x01\x01\x12\x17\n\nupdated_by\x18\x07 \x01(\tH\x05\x88\x01\x01\x12\x13\n\x06status\x18\x08 \x01(\x08H\x06\x88\x01\x01\x12\x32\n\x10sleep_stage_tags\x18\t \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12\x36\n\x11\x63ontent_form_tags\x18\n \x03(\x0b\x32\x1b.uburnode.v1.ContentFormTag\x12\x30\n\x0emechanism_tags\x18\x0b \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12@\n\x16\x61udio_engineering_tags\x18\x0c \x03(\x0b\x32 .uburnode.v1.AudioEngineeringTag\x12\x33\n\x11medical_risk_tags\x18\r \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12\x35\n\x13\x65vidence_level_tags\x18\x0e \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12\x11\n\tembedding\x18\x0f \x03(\x01\x42\r\n\x0b_audio_nameB\x0c\n\n_audio_urlB\x0e\n\x0c_descriptionB\x11\n\x0f_operation_typeB\r\n\x0b_created_byB\r\n\x0b_updated_byB\t\n\x07_status\"\x9d\x01\n\x0eSearchAudioReq\x12\x17\n\nquery_text\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x10sleep_stage_tags\x18\x02 \x03(\t\x12\x14\n\x0c\x63ontent_tags\x18\x03 \x03(\t\x12\x15\n\rdisliked_tags\x18\x04 \x03(\t\x12\x12\n\x05top_k\x18\x05 \x01(\x05H\x01\x88\x01\x01\x42\r\n\x0b_query_textB\x08\n\x06_top_k\"\xb0\x04\n\rAudioMaterial\x12\n\n\x02id\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x08\x12\x13\n\x0b\x63reate_time\x18\x04 \x01(\t\x12\x13\n\x0bupdate_time\x18\x05 \x01(\t\x12\x12\n\naudio_name\x18\x06 \x01(\t\x12\x11\n\taudio_url\x18\x07 \x01(\t\x12\x16\n\x0eoperation_type\x18\x08 \x01(\x05\x12\x12\n\ncreated_by\x18\t \x01(\t\x12\x12\n\nupdated_by\x18\n \x01(\t\x12\x32\n\x10sleep_stage_tags\x18\x0b \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12\x36\n\x11\x63ontent_form_tags\x18\x0c \x03(\x0b\x32\x1b.uburnode.v1.ContentFormTag\x12\x30\n\x0emechanism_tags\x18\r \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12@\n\x16\x61udio_engineering_tags\x18\x0e \x03(\x0b\x32 .uburnode.v1.AudioEngineeringTag\x12\x33\n\x11medical_risk_tags\x18\x0f \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12\x35\n\x13\x65vidence_level_tags\x18\x10 \x03(\x0b\x32\x18.uburnode.v1.SomniTagRef\x12\x11\n\tembedding\x18\x11 \x03(\x01\"@\n\x10\x41udioMaterialRes\x12,\n\x08material\x18\x01 \x01(\x0b\x32\x1a.uburnode.v1.AudioMaterial\"<\n\x0eSearchAudioRes\x12*\n\tmaterials\x18\x01 \x03(\x0b\x32\x17.google.protobuf.Struct\".\n\x0cGetAnswerReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x11\n\tanswer_id\x18\x02 \x01(\t\"\x0e\n\x0cGetAnswerRes2\xb5\x02\n\x0c\x41udioService\x12I\n\x0b\x43reateAudio\x12\x1b.uburnode.v1.CreateAudioReq\x1a\x1d.uburnode.v1.AudioMaterialRes\x12J\n\x0bUpdateAudio\x12\x1b.uburnode.v1.UpdateAudioReq\x1a\x1e.uburnode.v1.OperationResponse\x12\x45\n\x0b\x44\x65leteAudio\x12\x16.uburnode.v1.IdRequest\x1a\x1e.uburnode.v1.OperationResponse\x12G\n\x0bSearchAudio\x12\x1b.uburnode.v1.SearchAudioReq\x1a\x1b.uburnode.v1.SearchAudioRes2P\n\x0bQuizService\x12\x41\n\tGetAnswer\x12\x19.uburnode.v1.GetAnswerReq\x1a\x19.uburnode.v1.GetAnswerResb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'uburnode_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_IDREQUEST']._serialized_start=61 + _globals['_IDREQUEST']._serialized_end=84 + _globals['_OPERATIONRESPONSE']._serialized_start=86 + _globals['_OPERATIONRESPONSE']._serialized_end=130 + _globals['_SOMNITAGREF']._serialized_start=132 + _globals['_SOMNITAGREF']._serialized_end=233 + _globals['_CONTENTFORMTAG']._serialized_start=236 + _globals['_CONTENTFORMTAG']._serialized_end=470 + _globals['_AUDIOENGINEERINGTAG']._serialized_start=473 + _globals['_AUDIOENGINEERINGTAG']._serialized_end=713 + _globals['_CREATEAUDIOREQ']._serialized_start=716 + _globals['_CREATEAUDIOREQ']._serialized_end=1343 + _globals['_UPDATEAUDIOREQ']._serialized_start=1346 + _globals['_UPDATEAUDIOREQ']._serialized_end=2014 + _globals['_SEARCHAUDIOREQ']._serialized_start=2017 + _globals['_SEARCHAUDIOREQ']._serialized_end=2174 + _globals['_AUDIOMATERIAL']._serialized_start=2177 + _globals['_AUDIOMATERIAL']._serialized_end=2737 + _globals['_AUDIOMATERIALRES']._serialized_start=2739 + _globals['_AUDIOMATERIALRES']._serialized_end=2803 + _globals['_SEARCHAUDIORES']._serialized_start=2805 + _globals['_SEARCHAUDIORES']._serialized_end=2865 + _globals['_GETANSWERREQ']._serialized_start=2867 + _globals['_GETANSWERREQ']._serialized_end=2913 + _globals['_GETANSWERRES']._serialized_start=2915 + _globals['_GETANSWERRES']._serialized_end=2929 + _globals['_AUDIOSERVICE']._serialized_start=2932 + _globals['_AUDIOSERVICE']._serialized_end=3241 + _globals['_QUIZSERVICE']._serialized_start=3243 + _globals['_QUIZSERVICE']._serialized_end=3323 +# @@protoc_insertion_point(module_scope) diff --git a/app/uburnode_grpc/grpc_gen/uburnode_pb2_grpc.py b/app/uburnode_grpc/grpc_gen/uburnode_pb2_grpc.py new file mode 100644 index 0000000..45d759f --- /dev/null +++ b/app/uburnode_grpc/grpc_gen/uburnode_pb2_grpc.py @@ -0,0 +1,298 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from . import uburnode_pb2 as uburnode__pb2 + +GRPC_GENERATED_VERSION = '1.81.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in uburnode_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class AudioServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.CreateAudio = channel.unary_unary( + '/uburnode.v1.AudioService/CreateAudio', + request_serializer=uburnode__pb2.CreateAudioReq.SerializeToString, + response_deserializer=uburnode__pb2.AudioMaterialRes.FromString, + _registered_method=True) + self.UpdateAudio = channel.unary_unary( + '/uburnode.v1.AudioService/UpdateAudio', + request_serializer=uburnode__pb2.UpdateAudioReq.SerializeToString, + response_deserializer=uburnode__pb2.OperationResponse.FromString, + _registered_method=True) + self.DeleteAudio = channel.unary_unary( + '/uburnode.v1.AudioService/DeleteAudio', + request_serializer=uburnode__pb2.IdRequest.SerializeToString, + response_deserializer=uburnode__pb2.OperationResponse.FromString, + _registered_method=True) + self.SearchAudio = channel.unary_unary( + '/uburnode.v1.AudioService/SearchAudio', + request_serializer=uburnode__pb2.SearchAudioReq.SerializeToString, + response_deserializer=uburnode__pb2.SearchAudioRes.FromString, + _registered_method=True) + + +class AudioServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def CreateAudio(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateAudio(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteAudio(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SearchAudio(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_AudioServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'CreateAudio': grpc.unary_unary_rpc_method_handler( + servicer.CreateAudio, + request_deserializer=uburnode__pb2.CreateAudioReq.FromString, + response_serializer=uburnode__pb2.AudioMaterialRes.SerializeToString, + ), + 'UpdateAudio': grpc.unary_unary_rpc_method_handler( + servicer.UpdateAudio, + request_deserializer=uburnode__pb2.UpdateAudioReq.FromString, + response_serializer=uburnode__pb2.OperationResponse.SerializeToString, + ), + 'DeleteAudio': grpc.unary_unary_rpc_method_handler( + servicer.DeleteAudio, + request_deserializer=uburnode__pb2.IdRequest.FromString, + response_serializer=uburnode__pb2.OperationResponse.SerializeToString, + ), + 'SearchAudio': grpc.unary_unary_rpc_method_handler( + servicer.SearchAudio, + request_deserializer=uburnode__pb2.SearchAudioReq.FromString, + response_serializer=uburnode__pb2.SearchAudioRes.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'uburnode.v1.AudioService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('uburnode.v1.AudioService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class AudioService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def CreateAudio(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.v1.AudioService/CreateAudio', + uburnode__pb2.CreateAudioReq.SerializeToString, + uburnode__pb2.AudioMaterialRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateAudio(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.v1.AudioService/UpdateAudio', + uburnode__pb2.UpdateAudioReq.SerializeToString, + uburnode__pb2.OperationResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def DeleteAudio(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.v1.AudioService/DeleteAudio', + uburnode__pb2.IdRequest.SerializeToString, + uburnode__pb2.OperationResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def SearchAudio(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.v1.AudioService/SearchAudio', + uburnode__pb2.SearchAudioReq.SerializeToString, + uburnode__pb2.SearchAudioRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + +class QuizServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetAnswer = channel.unary_unary( + '/uburnode.v1.QuizService/GetAnswer', + request_serializer=uburnode__pb2.GetAnswerReq.SerializeToString, + response_deserializer=uburnode__pb2.GetAnswerRes.FromString, + _registered_method=True) + + +class QuizServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def GetAnswer(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_QuizServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetAnswer': grpc.unary_unary_rpc_method_handler( + servicer.GetAnswer, + request_deserializer=uburnode__pb2.GetAnswerReq.FromString, + response_serializer=uburnode__pb2.GetAnswerRes.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'uburnode.v1.QuizService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('uburnode.v1.QuizService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class QuizService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def GetAnswer(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.v1.QuizService/GetAnswer', + uburnode__pb2.GetAnswerReq.SerializeToString, + uburnode__pb2.GetAnswerRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/app/uburnode_grpc/grpc_gen/uburnode_somni_pb2.py b/app/uburnode_grpc/grpc_gen/uburnode_somni_pb2.py new file mode 100644 index 0000000..911ec47 --- /dev/null +++ b/app/uburnode_grpc/grpc_gen/uburnode_somni_pb2.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: uburnode_somni.proto +# Protobuf Python Version: 6.33.5 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 5, + '', + 'uburnode_somni.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14uburnode_somni.proto\x12\x11uburnode.somni.v1\x1a\x1cgoogle/protobuf/struct.proto\".\n\x0cGetAnswerReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x11\n\tanswer_id\x18\x02 \x01(\t\"\x1f\n\x0cGetAnswerRes\x12\x0f\n\x07\x61nswers\x18\x01 \x01(\t\"1\n\rReportDateReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0brecord_date\x18\x02 \x01(\t\"\xaa\x01\n\x0cSleepSummary\x12\x14\n\x0c\x62ody_battery\x18\x01 \x01(\x05\x12\x1b\n\x13\x62ody_battery_status\x18\x02 \x01(\t\x12\x15\n\rtotal_minutes\x18\x03 \x01(\x05\x12\x1a\n\x12\x64\x65\x65p_sleep_minutes\x18\x04 \x01(\x05\x12\x16\n\x0e\x61vg_heart_rate\x18\x05 \x01(\x05\x12\x1c\n\x14\x61vg_respiratory_rate\x18\x06 \x01(\x05\"G\n\rGetSummaryRes\x12\x36\n\rsleep_summary\x18\x01 \x01(\x0b\x32\x1f.uburnode.somni.v1.SleepSummary\"}\n\x10SleepEventDetail\x12\x12\n\nevent_type\x18\x01 \x01(\t\x12\x10\n\x08\x64uration\x18\x02 \x01(\t\x12\x15\n\rtrigger_cause\x18\x03 \x01(\t\x12\x14\n\x0c\x61\x63tion_taken\x18\x04 \x01(\t\x12\x16\n\x0eresult_summary\x18\x05 \x01(\t\"\x9b\x01\n\x0cIntervention\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x12\n\nevent_time\x18\x02 \x01(\t\x12\x12\n\nevent_type\x18\x03 \x01(\t\x12\x10\n\x08\x64uration\x18\x04 \x01(\t\x12\x15\n\rtrigger_cause\x18\x05 \x01(\t\x12\x14\n\x0c\x61\x63tion_taken\x18\x06 \x01(\t\x12\x16\n\x0eresult_summary\x18\x07 \x01(\t\"\xac\x01\n\x0eSleepEventItem\x12\x12\n\nevent_time\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0c\n\x04\x63ode\x18\x03 \x01(\t\x12\x33\n\x06\x65vents\x18\x04 \x03(\x0b\x32#.uburnode.somni.v1.SleepEventDetail\x12\x35\n\x0cintervention\x18\x05 \x01(\x0b\x32\x1f.uburnode.somni.v1.Intervention\"5\n\x08IdfStage\x12\r\n\x05stage\x18\x01 \x01(\t\x12\r\n\x05start\x18\x02 \x01(\t\x12\x0b\n\x03\x65nd\x18\x03 \x01(\t\"=\n\rPhysioMetrics\x12\x12\n\nheart_rate\x18\x01 \x01(\x05\x12\x18\n\x10respiration_rate\x18\x02 \x01(\x05\"Z\n\x0fPhysioDataPoint\x12\x14\n\x0c\x63ollected_at\x18\x01 \x01(\t\x12\x31\n\x07metrics\x18\x02 \x01(\x0b\x32 .uburnode.somni.v1.PhysioMetrics\"o\n\x0c\x45nvDataPoint\x12\x14\n\x0c\x63ollected_at\x18\x01 \x01(\t\x12\x13\n\x0btemperature\x18\x02 \x01(\x05\x12\x10\n\x08humidity\x18\x03 \x01(\x05\x12\x13\n\x0billuminance\x18\x04 \x01(\x05\x12\r\n\x05noise\x18\x05 \x01(\x05\"\xc0\x02\n\x0cGetEventsRes\x12\x13\n\x0brecord_date\x18\x01 \x01(\t\x12\x37\n\x0csleep_events\x18\x02 \x03(\x0b\x32!.uburnode.somni.v1.SleepEventItem\x12\x13\n\x0b\x65vent_count\x18\x03 \x01(\x05\x12\x16\n\x0e\x61\x62normal_count\x18\x04 \x01(\x05\x12\x1a\n\x12intervention_count\x18\x05 \x01(\x05\x12-\n\x08idf_data\x18\x06 \x03(\x0b\x32\x1b.uburnode.somni.v1.IdfStage\x12\x37\n\x0bphysio_data\x18\x07 \x03(\x0b\x32\".uburnode.somni.v1.PhysioDataPoint\x12\x31\n\x08\x65nv_data\x18\x08 \x03(\x0b\x32\x1f.uburnode.somni.v1.EnvDataPoint\"4\n\tEnvMetric\x12\r\n\x05value\x18\x01 \x01(\x05\x12\x0b\n\x03min\x18\x02 \x01(\x05\x12\x0b\n\x03max\x18\x03 \x01(\x05\"\xd7\x01\n\x12\x45nvironmentSummary\x12\x31\n\x0btemperature\x18\x01 \x01(\x0b\x32\x1c.uburnode.somni.v1.EnvMetric\x12.\n\x08humidity\x18\x02 \x01(\x0b\x32\x1c.uburnode.somni.v1.EnvMetric\x12\x31\n\x0billuminance\x18\x03 \x01(\x0b\x32\x1c.uburnode.somni.v1.EnvMetric\x12+\n\x05noise\x18\x04 \x01(\x0b\x32\x1c.uburnode.somni.v1.EnvMetric\"W\n\x11GetEnvironmentRes\x12\x42\n\x13\x65nvironment_summary\x18\x01 \x01(\x0b\x32%.uburnode.somni.v1.EnvironmentSummary\"2\n\x0eSleepStagePart\x12\x0f\n\x07minutes\x18\x01 \x01(\x05\x12\x0f\n\x07percent\x18\x02 \x01(\x05\"\xe7\x01\n\x0eSleepStructure\x12\x30\n\x05\x61wake\x18\x01 \x01(\x0b\x32!.uburnode.somni.v1.SleepStagePart\x12\x34\n\trem_sleep\x18\x02 \x01(\x0b\x32!.uburnode.somni.v1.SleepStagePart\x12\x36\n\x0blight_sleep\x18\x03 \x01(\x0b\x32!.uburnode.somni.v1.SleepStagePart\x12\x35\n\ndeep_sleep\x18\x04 \x01(\x0b\x32!.uburnode.somni.v1.SleepStagePart\"M\n\x0fGetStructureRes\x12:\n\x0fsleep_structure\x18\x01 \x01(\x0b\x32!.uburnode.somni.v1.SleepStructure\"\xb4\x01\n\x0cSleepQuality\x12\x1b\n\x13time_in_bed_minutes\x18\x01 \x01(\x05\x12#\n\x1bsleep_onset_latency_minutes\x18\x02 \x01(\x05\x12\x18\n\x10sleep_efficiency\x18\x03 \x01(\x05\x12\x0f\n\x07\x62\x65\x64time\x18\x04 \x01(\t\x12\x14\n\x0cwake_up_time\x18\x05 \x01(\t\x12!\n\x19\x61wake_after_onset_minutes\x18\x06 \x01(\x05\"L\n\x12GetSleepQualityRes\x12\x36\n\rsleep_quality\x18\x01 \x01(\x0b\x32\x1f.uburnode.somni.v1.SleepQuality\"\xc1\x01\n\x0bGetAudioReq\x12\x11\n\x04page\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\tpage_size\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12\x16\n\tfetch_all\x18\x03 \x01(\x08H\x02\x88\x01\x01\x12\x17\n\nquery_text\x18\x04 \x01(\tH\x03\x88\x01\x01\x12\x15\n\x08tag_code\x18\x05 \x01(\tH\x04\x88\x01\x01\x42\x07\n\x05_pageB\x0c\n\n_page_sizeB\x0c\n\n_fetch_allB\r\n\x0b_query_textB\x0b\n\t_tag_code\"\xdf\x01\n\rAudioListItem\x12\x0f\n\x02id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x17\n\naudio_name\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x16\n\taudio_url\x18\x03 \x01(\tH\x02\x88\x01\x01\x12\x16\n\tcover_url\x18\x04 \x01(\tH\x03\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x04\x88\x01\x01\x12\x10\n\x03vip\x18\x06 \x01(\x05H\x05\x88\x01\x01\x42\x05\n\x03_idB\r\n\x0b_audio_nameB\x0c\n\n_audio_urlB\x0c\n\n_cover_urlB\x0e\n\x0c_descriptionB\x06\n\x04_vip\"m\n\x0bGetAudioRes\x12.\n\x04list\x18\x01 \x03(\x0b\x32 .uburnode.somni.v1.AudioListItem\x12\x0c\n\x04page\x18\x02 \x01(\x05\x12\x11\n\tpage_size\x18\x03 \x01(\x05\x12\r\n\x05total\x18\x04 \x01(\x05\"\x10\n\x0eGetAudioTagReq\"\x93\x01\n\x0bTagDictItem\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0f\n\x07name_en\x18\x04 \x01(\t\x12\n\n\x02id\x18\x05 \x01(\t\x12-\n\rparent_tag_id\x18\x06 \x01(\x0b\x32\x16.google.protobuf.Value\x12\x0e\n\x06status\x18\x07 \x01(\t\">\n\x0eGetAudioTagRes\x12,\n\x04tags\x18\x01 \x03(\x0b\x32\x1e.uburnode.somni.v1.TagDictItem\"\x0b\n\tGetHotReq\",\n\nHotKeyword\x12\x0f\n\x07keyword\x18\x01 \x01(\t\x12\r\n\x05score\x18\x02 \x01(\x03\"9\n\tGetHotRes\x12,\n\x05items\x18\x01 \x03(\x0b\x32\x1d.uburnode.somni.v1.HotKeyword2\\\n\x0bQuizService\x12M\n\tGetAnswer\x12\x1f.uburnode.somni.v1.GetAnswerReq\x1a\x1f.uburnode.somni.v1.GetAnswerRes2\xbd\x03\n\rReportService\x12P\n\nGetSummary\x12 .uburnode.somni.v1.ReportDateReq\x1a .uburnode.somni.v1.GetSummaryRes\x12N\n\tGetEvents\x12 .uburnode.somni.v1.ReportDateReq\x1a\x1f.uburnode.somni.v1.GetEventsRes\x12X\n\x0eGetEnvironment\x12 .uburnode.somni.v1.ReportDateReq\x1a$.uburnode.somni.v1.GetEnvironmentRes\x12T\n\x0cGetStructure\x12 .uburnode.somni.v1.ReportDateReq\x1a\".uburnode.somni.v1.GetStructureRes\x12Z\n\x0fGetSleepQuality\x12 .uburnode.somni.v1.ReportDateReq\x1a%.uburnode.somni.v1.GetSleepQualityRes2\xf5\x01\n\x0c\x41udioService\x12J\n\x08GetAudio\x12\x1e.uburnode.somni.v1.GetAudioReq\x1a\x1e.uburnode.somni.v1.GetAudioRes\x12S\n\x0bGetAudioTag\x12!.uburnode.somni.v1.GetAudioTagReq\x1a!.uburnode.somni.v1.GetAudioTagRes\x12\x44\n\x06GetHot\x12\x1c.uburnode.somni.v1.GetHotReq\x1a\x1c.uburnode.somni.v1.GetHotResb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'uburnode_somni_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_GETANSWERREQ']._serialized_start=73 + _globals['_GETANSWERREQ']._serialized_end=119 + _globals['_GETANSWERRES']._serialized_start=121 + _globals['_GETANSWERRES']._serialized_end=152 + _globals['_REPORTDATEREQ']._serialized_start=154 + _globals['_REPORTDATEREQ']._serialized_end=203 + _globals['_SLEEPSUMMARY']._serialized_start=206 + _globals['_SLEEPSUMMARY']._serialized_end=376 + _globals['_GETSUMMARYRES']._serialized_start=378 + _globals['_GETSUMMARYRES']._serialized_end=449 + _globals['_SLEEPEVENTDETAIL']._serialized_start=451 + _globals['_SLEEPEVENTDETAIL']._serialized_end=576 + _globals['_INTERVENTION']._serialized_start=579 + _globals['_INTERVENTION']._serialized_end=734 + _globals['_SLEEPEVENTITEM']._serialized_start=737 + _globals['_SLEEPEVENTITEM']._serialized_end=909 + _globals['_IDFSTAGE']._serialized_start=911 + _globals['_IDFSTAGE']._serialized_end=964 + _globals['_PHYSIOMETRICS']._serialized_start=966 + _globals['_PHYSIOMETRICS']._serialized_end=1027 + _globals['_PHYSIODATAPOINT']._serialized_start=1029 + _globals['_PHYSIODATAPOINT']._serialized_end=1119 + _globals['_ENVDATAPOINT']._serialized_start=1121 + _globals['_ENVDATAPOINT']._serialized_end=1232 + _globals['_GETEVENTSRES']._serialized_start=1235 + _globals['_GETEVENTSRES']._serialized_end=1555 + _globals['_ENVMETRIC']._serialized_start=1557 + _globals['_ENVMETRIC']._serialized_end=1609 + _globals['_ENVIRONMENTSUMMARY']._serialized_start=1612 + _globals['_ENVIRONMENTSUMMARY']._serialized_end=1827 + _globals['_GETENVIRONMENTRES']._serialized_start=1829 + _globals['_GETENVIRONMENTRES']._serialized_end=1916 + _globals['_SLEEPSTAGEPART']._serialized_start=1918 + _globals['_SLEEPSTAGEPART']._serialized_end=1968 + _globals['_SLEEPSTRUCTURE']._serialized_start=1971 + _globals['_SLEEPSTRUCTURE']._serialized_end=2202 + _globals['_GETSTRUCTURERES']._serialized_start=2204 + _globals['_GETSTRUCTURERES']._serialized_end=2281 + _globals['_SLEEPQUALITY']._serialized_start=2284 + _globals['_SLEEPQUALITY']._serialized_end=2464 + _globals['_GETSLEEPQUALITYRES']._serialized_start=2466 + _globals['_GETSLEEPQUALITYRES']._serialized_end=2542 + _globals['_GETAUDIOREQ']._serialized_start=2545 + _globals['_GETAUDIOREQ']._serialized_end=2738 + _globals['_AUDIOLISTITEM']._serialized_start=2741 + _globals['_AUDIOLISTITEM']._serialized_end=2964 + _globals['_GETAUDIORES']._serialized_start=2966 + _globals['_GETAUDIORES']._serialized_end=3075 + _globals['_GETAUDIOTAGREQ']._serialized_start=3077 + _globals['_GETAUDIOTAGREQ']._serialized_end=3093 + _globals['_TAGDICTITEM']._serialized_start=3096 + _globals['_TAGDICTITEM']._serialized_end=3243 + _globals['_GETAUDIOTAGRES']._serialized_start=3245 + _globals['_GETAUDIOTAGRES']._serialized_end=3307 + _globals['_GETHOTREQ']._serialized_start=3309 + _globals['_GETHOTREQ']._serialized_end=3320 + _globals['_HOTKEYWORD']._serialized_start=3322 + _globals['_HOTKEYWORD']._serialized_end=3366 + _globals['_GETHOTRES']._serialized_start=3368 + _globals['_GETHOTRES']._serialized_end=3425 + _globals['_QUIZSERVICE']._serialized_start=3427 + _globals['_QUIZSERVICE']._serialized_end=3519 + _globals['_REPORTSERVICE']._serialized_start=3522 + _globals['_REPORTSERVICE']._serialized_end=3967 + _globals['_AUDIOSERVICE']._serialized_start=3970 + _globals['_AUDIOSERVICE']._serialized_end=4215 +# @@protoc_insertion_point(module_scope) diff --git a/app/uburnode_grpc/grpc_gen/uburnode_somni_pb2_grpc.py b/app/uburnode_grpc/grpc_gen/uburnode_somni_pb2_grpc.py new file mode 100644 index 0000000..b475e9e --- /dev/null +++ b/app/uburnode_grpc/grpc_gen/uburnode_somni_pb2_grpc.py @@ -0,0 +1,499 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from . import uburnode_somni_pb2 as uburnode__somni__pb2 + +GRPC_GENERATED_VERSION = '1.81.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in uburnode_somni_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class QuizServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetAnswer = channel.unary_unary( + '/uburnode.somni.v1.QuizService/GetAnswer', + request_serializer=uburnode__somni__pb2.GetAnswerReq.SerializeToString, + response_deserializer=uburnode__somni__pb2.GetAnswerRes.FromString, + _registered_method=True) + + +class QuizServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def GetAnswer(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_QuizServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetAnswer': grpc.unary_unary_rpc_method_handler( + servicer.GetAnswer, + request_deserializer=uburnode__somni__pb2.GetAnswerReq.FromString, + response_serializer=uburnode__somni__pb2.GetAnswerRes.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'uburnode.somni.v1.QuizService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('uburnode.somni.v1.QuizService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class QuizService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def GetAnswer(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.somni.v1.QuizService/GetAnswer', + uburnode__somni__pb2.GetAnswerReq.SerializeToString, + uburnode__somni__pb2.GetAnswerRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + +class ReportServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetSummary = channel.unary_unary( + '/uburnode.somni.v1.ReportService/GetSummary', + request_serializer=uburnode__somni__pb2.ReportDateReq.SerializeToString, + response_deserializer=uburnode__somni__pb2.GetSummaryRes.FromString, + _registered_method=True) + self.GetEvents = channel.unary_unary( + '/uburnode.somni.v1.ReportService/GetEvents', + request_serializer=uburnode__somni__pb2.ReportDateReq.SerializeToString, + response_deserializer=uburnode__somni__pb2.GetEventsRes.FromString, + _registered_method=True) + self.GetEnvironment = channel.unary_unary( + '/uburnode.somni.v1.ReportService/GetEnvironment', + request_serializer=uburnode__somni__pb2.ReportDateReq.SerializeToString, + response_deserializer=uburnode__somni__pb2.GetEnvironmentRes.FromString, + _registered_method=True) + self.GetStructure = channel.unary_unary( + '/uburnode.somni.v1.ReportService/GetStructure', + request_serializer=uburnode__somni__pb2.ReportDateReq.SerializeToString, + response_deserializer=uburnode__somni__pb2.GetStructureRes.FromString, + _registered_method=True) + self.GetSleepQuality = channel.unary_unary( + '/uburnode.somni.v1.ReportService/GetSleepQuality', + request_serializer=uburnode__somni__pb2.ReportDateReq.SerializeToString, + response_deserializer=uburnode__somni__pb2.GetSleepQualityRes.FromString, + _registered_method=True) + + +class ReportServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def GetSummary(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetEvents(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetEnvironment(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetStructure(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetSleepQuality(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_ReportServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetSummary': grpc.unary_unary_rpc_method_handler( + servicer.GetSummary, + request_deserializer=uburnode__somni__pb2.ReportDateReq.FromString, + response_serializer=uburnode__somni__pb2.GetSummaryRes.SerializeToString, + ), + 'GetEvents': grpc.unary_unary_rpc_method_handler( + servicer.GetEvents, + request_deserializer=uburnode__somni__pb2.ReportDateReq.FromString, + response_serializer=uburnode__somni__pb2.GetEventsRes.SerializeToString, + ), + 'GetEnvironment': grpc.unary_unary_rpc_method_handler( + servicer.GetEnvironment, + request_deserializer=uburnode__somni__pb2.ReportDateReq.FromString, + response_serializer=uburnode__somni__pb2.GetEnvironmentRes.SerializeToString, + ), + 'GetStructure': grpc.unary_unary_rpc_method_handler( + servicer.GetStructure, + request_deserializer=uburnode__somni__pb2.ReportDateReq.FromString, + response_serializer=uburnode__somni__pb2.GetStructureRes.SerializeToString, + ), + 'GetSleepQuality': grpc.unary_unary_rpc_method_handler( + servicer.GetSleepQuality, + request_deserializer=uburnode__somni__pb2.ReportDateReq.FromString, + response_serializer=uburnode__somni__pb2.GetSleepQualityRes.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'uburnode.somni.v1.ReportService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('uburnode.somni.v1.ReportService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class ReportService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def GetSummary(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.somni.v1.ReportService/GetSummary', + uburnode__somni__pb2.ReportDateReq.SerializeToString, + uburnode__somni__pb2.GetSummaryRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetEvents(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.somni.v1.ReportService/GetEvents', + uburnode__somni__pb2.ReportDateReq.SerializeToString, + uburnode__somni__pb2.GetEventsRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetEnvironment(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.somni.v1.ReportService/GetEnvironment', + uburnode__somni__pb2.ReportDateReq.SerializeToString, + uburnode__somni__pb2.GetEnvironmentRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetStructure(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.somni.v1.ReportService/GetStructure', + uburnode__somni__pb2.ReportDateReq.SerializeToString, + uburnode__somni__pb2.GetStructureRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetSleepQuality(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.somni.v1.ReportService/GetSleepQuality', + uburnode__somni__pb2.ReportDateReq.SerializeToString, + uburnode__somni__pb2.GetSleepQualityRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + +class AudioServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetAudio = channel.unary_unary( + '/uburnode.somni.v1.AudioService/GetAudio', + request_serializer=uburnode__somni__pb2.GetAudioReq.SerializeToString, + response_deserializer=uburnode__somni__pb2.GetAudioRes.FromString, + _registered_method=True) + self.GetAudioTag = channel.unary_unary( + '/uburnode.somni.v1.AudioService/GetAudioTag', + request_serializer=uburnode__somni__pb2.GetAudioTagReq.SerializeToString, + response_deserializer=uburnode__somni__pb2.GetAudioTagRes.FromString, + _registered_method=True) + self.GetHot = channel.unary_unary( + '/uburnode.somni.v1.AudioService/GetHot', + request_serializer=uburnode__somni__pb2.GetHotReq.SerializeToString, + response_deserializer=uburnode__somni__pb2.GetHotRes.FromString, + _registered_method=True) + + +class AudioServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def GetAudio(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetAudioTag(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetHot(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_AudioServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetAudio': grpc.unary_unary_rpc_method_handler( + servicer.GetAudio, + request_deserializer=uburnode__somni__pb2.GetAudioReq.FromString, + response_serializer=uburnode__somni__pb2.GetAudioRes.SerializeToString, + ), + 'GetAudioTag': grpc.unary_unary_rpc_method_handler( + servicer.GetAudioTag, + request_deserializer=uburnode__somni__pb2.GetAudioTagReq.FromString, + response_serializer=uburnode__somni__pb2.GetAudioTagRes.SerializeToString, + ), + 'GetHot': grpc.unary_unary_rpc_method_handler( + servicer.GetHot, + request_deserializer=uburnode__somni__pb2.GetHotReq.FromString, + response_serializer=uburnode__somni__pb2.GetHotRes.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'uburnode.somni.v1.AudioService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('uburnode.somni.v1.AudioService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class AudioService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def GetAudio(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.somni.v1.AudioService/GetAudio', + uburnode__somni__pb2.GetAudioReq.SerializeToString, + uburnode__somni__pb2.GetAudioRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetAudioTag(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.somni.v1.AudioService/GetAudioTag', + uburnode__somni__pb2.GetAudioTagReq.SerializeToString, + uburnode__somni__pb2.GetAudioTagRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetHot(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/uburnode.somni.v1.AudioService/GetHot', + uburnode__somni__pb2.GetHotReq.SerializeToString, + uburnode__somni__pb2.GetHotRes.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/docker-compose.yml b/docker-compose.yml index d8331fa..2b6d346 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,11 @@ services: environment: ES_NODE: http://elasticsearch:9200 REDIS_URL: redis://redis:6379/0 + SOMNI_REDIS_URL: redis://redis-somni:6379/0 + ports: + - "50065:50065" # 功能手板 gRPC + - "50064:50064" # 量产 gRPC + # HTTP 仍经 nginx :8001 volumes: - hf_cache:/data/huggingface - ./logs:/app/logs # 宿主机可直接看项目目录 logs/YYYY-MM-DD_ubur_log @@ -39,6 +44,8 @@ services: condition: service_healthy redis: condition: service_healthy + redis-somni: + condition: service_healthy restart: unless-stopped redis: @@ -52,6 +59,19 @@ services: retries: 10 restart: unless-stopped + redis-somni: + image: redis:7.4-alpine + container_name: uburnode-redis-somni + command: ["redis-server", "--appendonly", "yes", "--appendfsync", "everysec"] + volumes: + - redis_somni_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 10 + restart: unless-stopped + elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0 container_name: uburnode-es @@ -76,3 +96,4 @@ services: volumes: es_data: hf_cache: + redis_somni_data: diff --git a/proto/bionode_comm.proto b/proto/bionode_comm.proto deleted file mode 100644 index cf1e366..0000000 --- a/proto/bionode_comm.proto +++ /dev/null @@ -1,2252 +0,0 @@ -syntax = "proto3"; - -package bionode.comm.v1; - -import "bionode_common.proto"; - -// ============================================================ -// Comm Service — 通用业务微服务 -// 端口: gRPC :50062 -// 包含: QuizService / SurveyService / ShareService / EventService -// ============================================================ - -// ==================== QuizService ==================== -// 题库管理(全局题目池) -// 对应表: quiz_questions - -service QuizService { - rpc GetActiveQuestions (GetActiveQuestionsReq) returns (QuestionListRes); - rpc ListQuestions (ListQuestionsReq) returns (QuestionListRes); - rpc GetQuestion (IdReq) returns (QuestionRes); - rpc CreateQuestion (CreateQuestionReq) returns (QuestionRes); - rpc UpdateQuestion (UpdateQuestionReq) returns (QuestionRes); - rpc UpdateQuestionStatus (UpdateQuestionStatusReq) returns (EmptyRes); - rpc DeleteQuestion (IdReq) returns (EmptyRes); - rpc GetQuestionsByIds (GetQuestionsByIdsRequest) returns (QuestionListRes); - - // ----- 人格配置 ----- - rpc GetPersonality (GetPersonalityReq) returns (PersonalityRes); - rpc GetPersonalityById (IdReq) returns (PersonalityRes); - rpc ListPersonalities (ListPersonalitiesReq) returns (PersonalityListRes); - rpc CreatePersonality (CreatePersonalityReq) returns (PersonalityRes); - rpc UpdatePersonality (UpdatePersonalityReq) returns (PersonalityRes); - rpc UpdatePersonalityStatus (UpdatePersonalityStatusReq) returns (EmptyRes); - rpc UpdatePersonalityPeriods (UpdatePersonalityPeriodsReq) returns (EmptyRes); - rpc DeletePersonality (IdReq) returns (EmptyRes); - rpc GetDistinctMhrCodes (EmptyRes) returns (MhrCodesRes); -} - -message MhrCodesRes { - repeated string codes = 1; -} - -// ==================== SurveyService ==================== -// 问卷编排 + 答题 + 结果 -// 对应表: quiz_surveys, quiz_answers, quiz_results - -service SurveyService { - // ----- 问卷编排 ----- - rpc GetSurvey (GetSurveyReq) returns (SurveyRes); - rpc GetSurveyByCode (GetSurveyByCodeReq) returns (SurveyRes); - rpc ListSurveys (ListSurveysReq) returns (SurveyListRes); - rpc CreateSurvey (CreateSurveyReq) returns (SurveyRes); - rpc UpdateSurvey (UpdateSurveyReq) returns (SurveyRes); - rpc UpdateSurveyStatus (UpdateSurveyStatusReq) returns (EmptyRes); - rpc DeleteSurvey (IdReq) returns (EmptyRes); - - // ----- 获取问卷完整题目(H5 拉取用) ----- - rpc GetSurveyWithQuestions (GetSurveyByCodeReq) returns (SurveyWithQuestionsRes); - - // ----- 答题 ----- - rpc SaveAnswer (SaveAnswerReq) returns (AnswerRes); - rpc SaveAnswerAlways (SaveAnswerReq) returns (AnswerRes); // 不按天去重,每次新建(App/Somni 用) - rpc GetAnswerProgress (GetAnswerProgressReq) returns (AnswerRes); - rpc ListAnswers (ListAnswersReq) returns (AnswerListRes); - rpc GetAnswerById (GetAnswerByIdReq) returns (AnswerRes); - - // ----- 测评结果 ----- - rpc ComputeMhrCodes (ComputeMhrCodesReq) returns (ComputeMhrCodesRes); // 仅计算人格编码,不落库(Somni 匿名提交用) - rpc SubmitQuiz (SubmitQuizReq) returns (QuizResultRes); - rpc H5SubmitQuiz (SubmitQuizReq) returns (QuizResultRes); // H5:三位人格精确匹配 - rpc SubmitScaleQuiz (SubmitScaleQuizReq) returns (QuizResultRes); - rpc GetLatestResult (GetLatestResultReq) returns (QuizResultRes); - rpc GetResultById (GetResultByIdReq) returns (QuizResultRes); - rpc ListResults (ListResultsReq) returns (QuizResultListRes); -} - -// ==================== ShareService ==================== -// 分享追踪 -// 对应表: shares, share_visits - -service ShareService { - rpc CreateShare (CreateShareReq) returns (ShareRes); - rpc GetShareLanding (GetShareLandingReq) returns (ShareLandingRes); - rpc RecordVisit (RecordVisitReq) returns (EmptyRes); - rpc ListShareTracking (ListShareTrackingReq) returns (ShareTrackingListRes); - - // ----- Admin 运营管理 ----- - rpc ListSharerRanking (ListSharerRankingReq) returns (SharerRankingListRes); - rpc ListSharesByUser (ListSharesByUserReq) returns (ShareListRes); - rpc ListShareVisits (ListShareVisitsReq) returns (ShareVisitListRes); -} - -// ==================== EventService ==================== -// 埋点日志与数据统计 -// 对应表: log_events - -service EventService { - rpc TrackEvent (TrackEventReq) returns (EmptyRes); - rpc BatchTrack (BatchTrackReq) returns (EmptyRes); - rpc GetOverview (GetOverviewReq) returns (OverviewRes); - rpc GetPageViewUV (GetPageViewUVReq) returns (CountRes); - rpc GetQuizCompleteUV (GetQuizCompleteUVReq) returns (CountRes); - rpc GetInteractionCount (GetInteractionCountReq) returns (CountRes); - rpc ListLogEvents (ListLogEventsReq) returns (ListLogEventsRes); - rpc GetEventTrend (GetEventTrendReq) returns (EventTrendRes); - rpc GetEventFilterOptions (GetEventFilterOptionsReq) returns (EventFilterOptionsRes); -} - -// ==================== SomniService ==================== -// 睡眠报告、方案、基线、融合数据 -// 对应表: somni_*(报告、会话、方案、事件、生理、环境等) - -service SomniService { - rpc SetAlarm (SomniSetAlarmReq) returns (SomniSetAlarmRes); - rpc GetPlan (SomniGetPlanReq) returns (SomniGetPlanRes); - rpc MarkPlanStarted (SomniMarkPlanStartedReq) returns (SomniMarkPlanStartedRes); - rpc MarkPlanStopped (SomniMarkPlanStoppedReq) returns (SomniMarkPlanStoppedRes); - /** App 伴睡开始:comm 编排绑定设备 + device-gateway StartSession(scheme_v1 → MQTT 在网关转换) */ - rpc StartSomniDeviceSession (SomniStartSomniDeviceSessionReq) returns (SomniStartSomniDeviceSessionRes); - /** App 伴睡结束:停会话、解绑 */ - rpc StopSomniDeviceSession (SomniStopSomniDeviceSessionReq) returns (SomniStopSomniDeviceSessionRes); - rpc GetFusion (SomniGetFusionReq) returns (SomniGetFusionRes); - rpc GetBaseline (SomniGetBaselineReq) returns (SomniGetBaselineRes); - rpc GetReport (SomniGetReportReq) returns (SomniGetReportRes); - /** 睡眠星球详情:record_date 空则按服务端当日(UTC 日历,与 GetReport 一致) */ - rpc GetSleepPlanet (SomniGetSleepPlanetReq) returns (SomniGetSleepPlanetRes); - /** 睡眠星球列表,按 record_date 倒序分页 */ - rpc ListSleepPlanets (SomniListSleepPlanetsReq) returns (SomniListSleepPlanetsRes); - rpc ValidateMonitorSession (SomniValidateMonitorSessionReq) returns (SomniValidateMonitorSessionRes); - rpc GetMonitorPayload (SomniGetMonitorPayloadReq) returns (SomniGetMonitorPayloadRes); - rpc GetUidByMhrCodes (SomniGetUidByMhrCodesReq) returns (SomniGetUidByMhrCodesRes); - rpc CreateQuizSession (SomniCreateQuizSessionReq) returns (SomniCreateQuizSessionRes); - /** 问卷提交后:Redis 覆盖当前 X-App-Instance-Id 对应的映射 uid */ - rpc UpsertSomniAppInstUser (SomniUpsertSomniAppInstUserReq) returns (SomniUpsertSomniAppInstUserRes); - /** 设备解绑时:删除 Redis 中该实例的 quiz_uid 映射 */ - rpc DeleteSomniAppInstUser (SomniDeleteSomniAppInstUserReq) returns (SomniUpsertSomniAppInstUserRes); - /** Somni Quiz AI 对话问卷(comm 编排 Init/Chat、Redis 快照;finalized 后等价 submit 落库) */ - rpc SomniQuizChat (SomniQuizChatReq) returns (SomniQuizChatRes); - - // ----- 干预配置 CRUD ----- - rpc ListInterventionConfigs (ListInterventionConfigsReq) returns (InterventionConfigListRes); - rpc GetInterventionConfig (IdReq) returns (InterventionConfigRes); - rpc UpsertInterventionConfig (UpsertInterventionConfigReq) returns (InterventionConfigRes); - rpc DeleteInterventionConfig (IdReq) returns (EmptyRes); - - // ----- 监控页查询接口 ----- - rpc GetPlanPhases (SomniGetPlanPhasesReq) returns (SomniGetPlanPhasesRes); - rpc GetInterventionEvents (SomniGetInterventionEventsReq) returns (SomniGetInterventionEventsRes); - rpc GetMonitorReportData (SomniGetMonitorReportDataReq) returns (SomniGetMonitorReportDataRes); - /** 监控端睡眠报告:调用 DPH ProcessRadarReport 生成真/假两份报告并写入 Redis 缓存 */ - rpc GenerateMonitorDphReports (SomniGenerateMonitorDphReportsReq) returns (SomniGenerateMonitorDphReportsRes); - - // ----- 控制台会话上下文 ----- - /** 根据 session_id 查询会话关联的人格编码和方案阶段,供控制台接管 APP 会话时加载 */ - rpc GetSessionContext (SomniGetSessionContextReq) returns (SomniGetSessionContextRes); - /** 控制台结束会话:停设备与调度;有 somni_plans 时同步 is_started=false */ - rpc ConsoleStopDeviceSession (SomniConsoleStopDeviceSessionReq) returns (SomniConsoleStopDeviceSessionRes); - - // ----- 控制台手动干预 ----- - rpc TriggerIntervention (TriggerInterventionReq) returns (EmptyRes); - rpc StopIntervention (StopInterventionReq) returns (EmptyRes); - /** 联调:清空 AI 思维滑动窗/冷却/patrol/序列(不抑制 patrol,不清 current_phase) */ - rpc ResetAiThoughtRehearsalState (ResetAiThoughtRehearsalStateReq) returns (EmptyRes); - /** 设备 MQTT 连断 — 推送 device_link 思维流(device stream,可无 session) */ - rpc EmitDeviceLinkAiThought (EmitDeviceLinkAiThoughtReq) returns (EmptyRes); - - // ----- 对话记录查询 ----- - rpc GetChatMessages (GetChatMessagesReq) returns (ChatMessageListRes); - rpc ListChatConversations (ListChatConversationsReq) returns (ChatConversationListRes); - rpc ListUserChatConversations (ListUserChatConversationsReq) returns (ChatConversationListRes); - - // ----- 临时方案 ----- - rpc GetTempPlan (SomniGetTempPlanReq) returns (SomniGetTempPlanRes); - - // ----- Somni 对话音频上传 ----- - rpc UploadChatAudio (UploadChatAudioReq) returns (UploadChatAudioRes); - - // ----- Somni 对话消息落库 ----- - rpc AppendChatMessage (AppendChatMessageReq) returns (EmptyRes); - - // ----- 睡眠地图(预设区级聚合 + 个人分析) ----- - rpc GetSleepMapCityOverview (SomniGetSleepMapCityOverviewReq) returns (SomniGetSleepMapCityOverviewRes); - rpc GetSleepMapHighlights (SomniGetSleepMapHighlightsReq) returns (SomniGetSleepMapHighlightsRes); - rpc GetSleepMapCityRanking (SomniGetSleepMapCityRankingReq) returns (SomniGetSleepMapCityRankingRes); - - // ----- DPH 控制台专用 ----- - /** 为 DPH 方案创建独立会话 + 方案记录(admin 控制台调用) */ - rpc CreateDphConsoleSession (CreateDphConsoleSessionReq) returns (CreateDphConsoleSessionRes); -} - -// ==================== AudioMaterialService ==================== -// 音频原料 & 分类/标签管理 -// 原料表: somni_audio_materials(Create/Update/Delete/Get/ListAudioMaterial*) -// 分类/标签字典表: somni_audio_tag_dictionary(List/Get/Create/Update/DeleteAudioMaterialCategory*) -// (H5 enrich 仍可能内部读取旧表 audio_materials,不暴露 Legacy RPC) - -service AudioMaterialService { - // ----- 分类/标签字典 somni_audio_tag_dictionary ----- - rpc ListAudioMaterialCategories (ListAudioMaterialCategoriesReq) returns (AudioMaterialCategoryListRes); - rpc GetAudioMaterialCategory (IdReq) returns (AudioMaterialCategoryRes); - rpc CreateAudioMaterialCategory (CreateAudioMaterialCategoryReq) returns (EmptyRes); - rpc UpdateAudioMaterialCategory (UpdateAudioMaterialCategoryReq) returns (EmptyRes); - rpc DeleteAudioMaterialCategory (IdReq) returns (EmptyRes); - - // ----- 原料 somni_audio_materials ----- - rpc ListAudioMaterials (ListAudioMaterialsReq) returns (AudioMaterialListRes); - rpc GetAudioMaterial (IdReq) returns (AudioMaterialRes); - rpc GetDistinctTags (EmptyReq) returns (DistinctTagsRes); - rpc CreateAudioMaterial (CreateAudioMaterialReq) returns (EmptyRes); - rpc UpdateAudioMaterial (UpdateAudioMaterialReq) returns (EmptyRes); - rpc UpdateAudioMaterialStatus (UpdateAudioMaterialStatusReq) returns (EmptyRes); - rpc DeleteAudioMaterial (IdReq) returns (EmptyRes); -} - -// ============================================================ -// 通用消息 -// ============================================================ - -message IdReq { - string id = 1; -} - -message EmptyReq {} - -message EmptyRes {} - -message CountRes { - int64 count = 1; -} - -// ============================================================ -// Quiz — 题库(题目)相关消息 -// ============================================================ - -message QuestionOption { - string option_id = 1; - string option_text = 2; - int32 sort_order = 3; - bool is_input_enabled = 4; -} - -// 配置子项:兼容 time/date_picker 的具名输入项 + composite 复合题的子题目 -// - time/date_picker 仅使用 index/label/format -// - composite 仅使用 index/title/description/input_type/options/config/is_extra_input -message PickerItem { - int32 index = 3; // 数组下标(0、1、2…),提交答题时直接使用 - string label = 1; // [picker] 具名输入标签,如「上床时间:」「起床时间:」 - string format = 2; // [picker] 该项格式,如 HH:mm、HH、YYYY-MM-DD,每项可不同 - - // [composite] 复合题子题目字段(同构于 QuestionInfo 的子集) - string title = 4; // 子题标签 - string description = 5; // 子题描述 - string input_type = 6; // 子题型(不能再是 composite) - repeated QuestionOption options = 7; // 子题选项 - QuestionConfig config = 8; // 子题配置(自引用,proto 支持) - bool is_extra_input = 9; // 子题级追加输入开关 -} - -message QuestionConfig { - int32 max_select_count = 1; // multiple_choice - string placeholder = 2; // text_input - int32 max_length = 3; // text_input - int32 min = 4; // number_input, slider - int32 max = 5; // number_input, slider - int32 step = 6; // number_input, slider - string unit = 7; // number_input - int32 max_value = 8; // rating - bool allow_half = 9; // rating - repeated string levels = 10; // cascading_selector - string data_source = 11; // cascading_selector - map labels = 12; // slider - repeated PickerItem items = 14; // time/date_picker 具名输入列表 / composite 复合题子题目列表 - string active_text = 15; // switch 开启文案 - string inactive_text = 16; // switch 关闭文案 -} - -message QuestionInfo { - string id = 1; - string title = 2; - string description = 3; - string input_type = 4; // single_choice, multiple_choice, text_input, etc. - string business_type = 5; // sleep, vital_signs, nutrition, general - string dimension = 6; - repeated QuestionOption options = 7; - QuestionConfig config = 8; - repeated string tags = 9; - string language = 10; - int32 status = 11; - string create_time = 12; - string update_time = 13; - string scoring_type = 14; // score | label | none - bool is_extra_input = 15; // 题目级追加输入开关 -} - -message GetActiveQuestionsReq { - string business_type = 1; - string language = 2; -} - -message ListQuestionsReq { - bionode.common.v1.PageRequest page = 1; // page/page_size/keyword/order_by - string business_type = 2; - string input_type = 3; - string dimension = 4; - int32 status = 5; // -1 表示不筛选 - string language = 6; -} - -message CreateQuestionReq { - string title = 1; - string description = 2; - string input_type = 3; - string business_type = 4; - string dimension = 5; - repeated QuestionOption options = 6; - QuestionConfig config = 7; - repeated string tags = 8; - string language = 9; - bool is_extra_input = 10; -} - -message UpdateQuestionReq { - string id = 1; - string title = 2; - string description = 3; - string input_type = 4; - string business_type = 5; - string dimension = 6; - repeated QuestionOption options = 7; - QuestionConfig config = 8; - repeated string tags = 9; - string language = 10; - int32 status = 11; - bool is_extra_input = 12; -} - -message UpdateQuestionStatusReq { - string id = 1; - int32 status = 2; -} - -message GetQuestionsByIdsRequest { - repeated string ids = 1; -} - -message QuestionRes { - QuestionInfo question = 1; -} - -message QuestionListRes { - repeated QuestionInfo questions = 1; - bionode.common.v1.PageResponse page = 2; -} - -// ============================================================ -// Survey — 问卷编排相关消息 -// ============================================================ - -message ItemDisplayRules { - string depends_on_question_id = 1; // 依赖的题目 ID - string option_id = 2; // 触发显示的选项 ID(如 "A") -} - -message SurveyPageItem { - string question_id = 1; - int32 sort_order = 2; - bool is_required = 3; - string alias = 4; - ItemDisplayRules display_rules = 5; // 条件显示规则,为空则始终显示 -} - -message SurveyPage { - int32 page_index = 1; - string page_title = 2; - string page_description = 3; - repeated SurveyPageItem items = 4; -} - -message SurveySettings { - bool allow_resume = 1; - bool show_progress = 2; - bool randomize_options = 3; - int32 time_limit_minutes = 4; -} - -message CodeRuleOption { - string option_id = 1; - int32 score = 2; - string label_value = 3; -} - -message CodeRuleQuestion { - string question_id = 1; - repeated CodeRuleOption options = 2; -} - -message ThresholdItem { - int32 min = 1; - int32 max = 2; - string label = 3; - string desc = 4; // 命中该阈值时的结果说明(用于评估报告展示) -} - -message CodeRule { - string code_key = 1; - string rule_type = 2; - int32 in_code = 3; - repeated CodeRuleQuestion questions = 4; - repeated ThresholdItem thresholds = 5; - string default_label = 6; - string rule_name = 7; // 规则中文名(用于后台展示) - double score_multiplier = 8; // 该规则汇总分倍率(如 SDS/SAS 的 1.25),未设置默认 1 -} - -message ResultGuideItem { - int32 min = 1; - int32 max = 2; - string label = 3; -} - -// 问卷级结果解释(H5 评估报告用于展示评分标准说明) -message ResultGuide { - string title = 1; - string content = 2; - repeated ResultGuideItem items = 3; -} - -message SurveyInfo { - string id = 1; - string title = 2; - string code = 3; - string description = 4; - string business_type = 5; - string cover_image = 6; - repeated SurveyPage pages = 7; - SurveySettings settings = 8; - int32 total_question_count = 9; - repeated CodeRule rule_config = 10; - string language = 11; - int32 status = 12; - string create_time = 13; - string update_time = 14; - ResultGuide result_guide = 15; // 问卷级结果解释(评估报告展示) -} - -message GetSurveyReq { - string id = 1; -} - -message GetSurveyByCodeReq { - string code = 1; - string language = 2; -} - -message ListSurveysReq { - bionode.common.v1.PageRequest page = 1; - string business_type = 2; - int32 status = 3; - string language = 4; -} - -message CreateSurveyReq { - string title = 1; - string code = 2; - string description = 3; - string business_type = 4; - string cover_image = 5; - repeated SurveyPage pages = 6; - SurveySettings settings = 7; - string language = 8; - repeated CodeRule rule_config = 9; - ResultGuide result_guide = 10; -} - -message UpdateSurveyReq { - string id = 1; - string title = 2; - string code = 3; - string description = 4; - string business_type = 5; - string cover_image = 6; - repeated SurveyPage pages = 7; - SurveySettings settings = 8; - string language = 9; - int32 status = 10; - repeated CodeRule rule_config = 11; - ResultGuide result_guide = 12; -} - -message UpdateSurveyStatusReq { - string id = 1; - int32 status = 2; -} - -message SurveyRes { - SurveyInfo survey = 1; -} - -message SurveyListRes { - repeated SurveyInfo surveys = 1; - bionode.common.v1.PageResponse page = 2; -} - -// 带完整题目的问卷(H5 拉取用) -message SurveyPageWithQuestions { - int32 page_index = 1; - string page_title = 2; - string page_description = 3; - repeated QuestionWithMeta questions = 4; -} - -message DisplayRules { - string depends_on = 1; // 依赖题目的 alias(如 "Q7") - string depends_on_question_id = 2; // 依赖题目的 question_id - string show_when_option = 3; // 当依赖题目选中此 option_id 时显示 -} - -message QuestionWithMeta { - QuestionInfo question = 1; - int32 sort_order = 2; - bool is_required = 3; - string alias = 4; - DisplayRules display_rules = 5; -} - -message SurveyWithQuestionsRes { - SurveyInfo survey = 1; - repeated SurveyPageWithQuestions pages = 2; -} - -// ============================================================ -// Quiz — 答题相关消息 -// ============================================================ - -message AnswerSelectedOption { - string opt_id = 1; // 选项/输入槽位 ID(选择类为 option_id 如 A,time/date 为下标如 0) - string opt_text = 2; // 展示文本(后台自动补全) - double score = 3; - string label_value = 4; - string input_value = 5; -} - -message AnswerDetail { - string question_id = 1; - string value_json = 7; // 统一答案,JSON:选择类=["A"],time/date=[{opt_id,input_value}],numeric=60 - string title = 5; - repeated string selected_options = 2; // 内部解析后填充,用于计分与存储 - string input_value = 3; - double numeric_value = 4; - repeated AnswerSelectedOption opt_snapshots = 6; -} - -message AnswerInfo { - string id = 1; - string uid = 2; - string survey_id = 3; - string survey_code = 4; - string language = 5; - repeated AnswerDetail answers = 6; - bool is_completed = 7; - string record_date = 8; - string create_time = 9; - string update_time = 10; - string nickname = 11; - string avatar_url = 12; -} - -message SaveAnswerReq { - string uid = 1; - string survey_code = 2; - repeated AnswerDetail answers = 3; -} - -message GetAnswerProgressReq { - string uid = 1; - string survey_code = 2; -} - -message GetAnswerByIdReq { - string answer_id = 1; -} - -message AnswerRes { - AnswerInfo answer = 1; -} - -message ListAnswersReq { - bionode.common.v1.PageRequest page = 1; - string uid = 2; - string survey_code = 3; - int32 is_completed = 4; - string start_date = 5; - string end_date = 6; -} - -message AnswerListRes { - repeated AnswerInfo answers = 1; - bionode.common.v1.PageResponse page = 2; -} - -// ============================================================ -// Quiz — 测评结果相关消息 -// ============================================================ - -message Indicators { - int32 circadian = 1; - int32 sensitivity = 2; - int32 brain_state = 3; - int32 atmosphere = 4; -} - -message ImproveScene { - string type = 1; - string name = 2; - string description = 3; - string config = 4; -} - -message ImprovePlan { - string phase = 1; - string phase_name = 2; - repeated ImproveScene scenes = 3; - string description = 4; -} - -message ScoreDetailEntry { - string dimension = 1; - int32 score = 2; -} - -message QuizResultInfo { - string id = 1; - string uid = 2; - string survey_id = 3; - string survey_code = 4; - string answer_id = 5; - repeated string mhr_codes = 6; - string mhr_name = 7; - string atmosphere = 8; - repeated string tags = 9; - string comment = 10; - Indicators indicators = 11; - string analysis_insight = 12; - double population_ratio = 13; - repeated ImprovePlan improve_plan = 14; - repeated ScoreDetailEntry score_detail = 15; - string create_time = 16; - string update_time = 17; - string nickname = 18; - string avatar_url = 19; - // 人格快照字段 - string identity_name = 20; - string character_3d_url = 21; - string status_analysis = 22; - IndicatorDescriptions indicator_descriptions = 23; - ShareConfig share_config = 24; - // 用户字段 - string openid = 25; - int32 gender = 26; -} - -message ComputeMhrCodesReq { - string survey_code = 1; - repeated AnswerDetail answers = 2; - string language = 3; -} - -message ComputeMhrCodesRes { - repeated string mhr_codes = 1; -} - -message SubmitQuizReq { - string uid = 1; - string survey_code = 2; - string answer_id = 3; - string language = 4; // 语言标识(zh/en),用于查询对应语言的人格配置 -} - -message SubmitScaleQuizReq { - string uid = 1; - string survey_code = 2; - string answer_id = 3; - string scale_uid = 4; - string language = 5; - string timepoint = 6; -} - -message GetLatestResultReq { - string uid = 1; - string survey_code = 2; -} - -message GetResultByIdReq { - string result_id = 1; // 结果 ID(MongoDB ObjectId) -} - -message ListResultsReq { - bionode.common.v1.PageRequest page = 1; - string uid = 2; - string survey_code = 3; - repeated string mhr_codes = 4; - string mhr_name = 5; -} - -message QuizResultRes { - QuizResultInfo result = 1; -} - -message QuizResultListRes { - repeated QuizResultInfo results = 1; - bionode.common.v1.PageResponse page = 2; -} - -// ============================================================ -// Quiz — 人格配置相关消息 -// ============================================================ - -message IndicatorDescriptions { - string circadian = 1; - string sensitivity = 2; - string brain_state = 3; - string atmosphere = 4; -} - -message SceneItem { - string type = 1; - string name = 2; - string description = 3; - string config = 4; -} - -// ----- 控制台 Scheme 子结构(新格式) ----- - -message SchemeColorStop { - int32 r = 1; - int32 g = 2; - int32 b = 3; - int32 ww = 4; - int32 cw = 5; - int32 lux = 6; - bool enabled = 7; -} - -message LightExitEffectTarget { - int32 r = 1; - int32 g = 2; - int32 b = 3; - int32 cw = 4; - int32 ww = 5; - int32 lux = 6; -} - -message LightExitEffect { - string type = 1; // fade_out | fade_to | none - int32 duration_ms = 2; - LightExitEffectTarget target = 3; -} - -message SchemeLight { - string mode = 1; - repeated SchemeColorStop color_stops = 2; - repeated int32 transition_durations = 3; - bool enabled = 4; - string description = 5; - LightExitEffect exit_effect = 6; -} - -message SchemeTrack { - string material_id = 1; - string name = 2; - string type = 3; - int32 volume = 4; - bool enabled = 5; -} - -message SchemeSound { - string mode = 1; - repeated SchemeTrack tracks = 2; - bool enabled = 3; - string description = 4; -} - -message SchemeScentSlot { - string name = 1; - string type = 2; - int32 release = 3; - int32 interval = 4; - string cycle_mode = 5; - bool enabled = 6; - string icon = 7; -} - -message SchemeScent { - repeated SchemeScentSlot slots = 1; - string mode = 2; - bool enabled = 3; - string description = 4; -} - -message SchemeTemp { - bool enabled = 1; - string mode = 2; // sleep | standard - int32 target_temp = 3; // 精确到 1℃ - string description = 4; -} - -message SchemeItem { - string name = 1; - SchemeLight light = 2; - SchemeSound sound = 3; - SchemeScent scent = 4; - SchemeTemp temp = 5; - int32 duration_sec = 6; // 节点时长(秒);多节点之和应等于阶段 duration_sec -} - -message Period { - string phase = 1; - string phase_name = 2; - repeated SceneItem scenes = 3; // 旧格式,兼容 H5/App - string description = 4; - int32 duration_sec = 5; - int32 transition_sec = 6; - repeated SchemeItem schemes = 7; // 新格式,控制台用 -} - -message ShareConfig { - string share_title_template = 1; - string share_desc_template = 2; - string share_image_url = 3; - string share_summary_template = 4; -} - -message PersonalityInfo { - string id = 1; - repeated string mhr_codes = 2; - string mhr_name = 3; - string identity_name = 4; - string status_analysis = 5; - string title = 6; - string description = 7; - string comment_template = 8; - string character_3d_url = 9; - double population_ratio = 10; - IndicatorDescriptions indicator_descriptions = 11; - repeated Period periods = 12; - ShareConfig share_config = 13; - string language = 14; - int32 sort_order = 15; - int32 status = 16; - string create_time = 17; - string update_time = 18; -} - -message GetPersonalityReq { - repeated string mhr_codes = 1; - string language = 2; -} - -message ListPersonalitiesReq { - bionode.common.v1.PageRequest page = 1; - string language = 2; - int32 status = 3; - repeated string mhr_codes = 4; -} - -message CreatePersonalityReq { - repeated string mhr_codes = 1; - string mhr_name = 2; - string identity_name = 3; - string status_analysis = 4; - string title = 5; - string description = 6; - string comment_template = 7; - string character_3d_url = 8; - double population_ratio = 9; - IndicatorDescriptions indicator_descriptions = 10; - repeated Period periods = 11; - ShareConfig share_config = 12; - string language = 13; - int32 sort_order = 14; -} - -message UpdatePersonalityReq { - string id = 1; - repeated string mhr_codes = 2; - string mhr_name = 3; - string identity_name = 4; - string status_analysis = 5; - string title = 6; - string description = 7; - string comment_template = 8; - string character_3d_url = 9; - double population_ratio = 10; - IndicatorDescriptions indicator_descriptions = 11; - repeated Period periods = 12; - ShareConfig share_config = 13; - string language = 14; - int32 sort_order = 15; - int32 status = 16; -} - -message UpdatePersonalityPeriodsReq { - string id = 1; - repeated Period periods = 2; -} - -message UpdatePersonalityStatusReq { - string id = 1; - int32 status = 2; -} - -message PersonalityRes { - PersonalityInfo personality = 1; -} - -message PersonalityListRes { - repeated PersonalityInfo personalities = 1; - bionode.common.v1.PageResponse page = 2; -} - -// ============================================================ -// Share — 分享相关消息 -// ============================================================ - -message ShareInfo { - string id = 1; - string uid = 2; - string share_id = 3; - string result_id = 4; - string share_type = 5; - string share_page = 6; - string create_time = 7; -} - -message CreateShareReq { - string uid = 1; - string result_id = 2; - string share_type = 3; - string share_page = 4; -} - -message ShareRes { - ShareInfo share = 1; -} - -message GetShareLandingReq { - string share_id = 1; -} - -message ShareLandingRes { - ShareInfo share = 1; - string sharer_name = 2; - QuizResultInfo result = 3; - PersonalityInfo personality = 4; -} - -message RecordVisitReq { - string share_id = 1; - string visitor_uid = 2; -} - -message ShareTrackingItem { - string share_id = 1; - string sharer_uid = 2; - string sharer_nickname = 3; - string sharer_mhr_name = 4; - int32 visit_count = 5; - int32 new_user_count = 6; - string create_time = 7; -} - -message ListShareTrackingReq { - bionode.common.v1.PageRequest page = 1; -} - -message ShareTrackingListRes { - repeated ShareTrackingItem trackings = 1; - bionode.common.v1.PageResponse page = 2; -} - -// ----- 运营管理:分享者排行 ----- - -message SharerRankingItem { - string sharer_uid = 1; // 分享者 uid - string sharer_nickname = 2; // 分享者昵称 - string sharer_avatar = 3; // 分享者头像 - string mhr_name = 4; // 人格名称(最近一次分享的结果) - int32 share_count = 5; // 该用户总分享次数 - int32 new_uv_count = 6; // 带来新用户数 - int32 total_visit_count = 7; // 总访问数 - string last_share_time = 8; // 最近分享时间 -} - -message ListSharerRankingReq { - bionode.common.v1.PageRequest page = 1; - string sharer_nickname = 2; // 分享者昵称模糊搜索 - string mhr_name = 3; // 人格名称模糊搜索 - string start_time = 4; // 时间范围开始 - string end_time = 5; // 时间范围结束 -} - -message SharerRankingListRes { - repeated SharerRankingItem rankings = 1; - bionode.common.v1.PageResponse page = 2; -} - -// ----- 运营管理:某用户的分享列表 ----- - -message ShareItem { - string id = 1; // shares._id - string share_id = 2; - string uid = 3; - string result_id = 4; - string share_type = 5; - string share_page = 6; - int32 visit_count = 7; // 该条分享的访问数 - int32 new_user_count = 8; // 该条分享带来的新用户数 - string create_time = 9; - string mhr_name = 10; // 人格名称 -} - -message ListSharesByUserReq { - string uid = 1; - bionode.common.v1.PageRequest page = 2; -} - -message ShareListRes { - repeated ShareItem shares = 1; - bionode.common.v1.PageResponse page = 2; -} - -// ----- 运营管理:某条分享的访客列表 ----- - -message ShareVisitItem { - string id = 1; // share_visits._id - string share_id = 2; - string visitor_uid = 3; - string visitor_nickname = 4; - string visitor_avatar = 5; - bool is_new_user = 6; - string create_time = 7; -} - -message ListShareVisitsReq { - string share_id = 1; - bionode.common.v1.PageRequest page = 2; -} - -message ShareVisitListRes { - repeated ShareVisitItem visits = 1; - bionode.common.v1.PageResponse page = 2; -} - -// ============================================================ -// Event — 埋点相关消息 -// ============================================================ - -message ClientInfo { - string ua = 1; - string platform = 2; - string screen = 3; - string network = 4; - string wechat_ver = 5; -} - -message TrackEventReq { - string uid = 1; - string app_id = 2; - string session_id = 3; - string event = 4; - string target_type = 5; - string target_id = 6; - string action = 7; - string label = 8; - double value = 9; - string page = 10; - string referrer = 11; - string metadata = 12; - ClientInfo client_info = 13; -} - -message BatchTrackReq { - repeated TrackEventReq events = 1; -} - -message GetOverviewReq { - string app_id = 1; - string start_date = 2; - string end_date = 3; -} - -message OverviewRes { - int64 page_view_uv = 1; - int64 quiz_complete_uv = 2; - double completion_rate = 3; - int64 interaction_count = 4; - int64 total_events = 5; -} - -message GetPageViewUVReq { - string app_id = 1; - string start_date = 2; - string end_date = 3; -} - -message GetQuizCompleteUVReq { - string app_id = 1; - string start_date = 2; - string end_date = 3; -} - -message GetInteractionCountReq { - string app_id = 1; - string start_date = 2; - string end_date = 3; -} - -message ListLogEventsReq { - bionode.common.v1.PageRequest page = 1; - string app_id = 2; - string event = 3; - string target_type = 4; - string target_id = 5; - string uid = 6; - // 注意:原 `string page` 已与公共 `PageRequest page` 字段名冲突,重命名为 page_path 表达页面路径语义 - string page_path = 7; - string start_date = 8; - string end_date = 9; -} - -message LogEventItem { - string id = 1; - string uid = 2; - string app_id = 3; - string session_id = 4; - string event = 5; - string target_type = 6; - string target_id = 7; - string action = 8; - string label = 9; - double value = 10; - string page = 11; - string referrer = 12; - string metadata = 13; - string create_time = 14; - string nickname = 15; - string avatar_url = 16; -} - -message ListLogEventsRes { - repeated LogEventItem events = 1; - bionode.common.v1.PageResponse page = 2; -} - -message GetEventTrendReq { - string app_id = 1; - string start_date = 2; - string end_date = 3; -} - -message EventTrendRes { - repeated string dates = 1; - repeated int64 pv_list = 2; - repeated int64 uv_list = 3; -} - -message GetEventFilterOptionsReq { - string app_id = 1; -} - -message EventFilterOptionsRes { - repeated string app_ids = 1; - repeated string pages = 2; -} - -// ============================================================ -// AudioMaterial — 音频原料相关消息 -// ============================================================ - -message AudioMaterialCategoryInfo { - string id = 1; - string name = 2; - string status = 3; // 库内字符串,如「启用」 - string create_time = 4; - string update_time = 5; - string type = 6; // sleep_stage / content_form / ... - string code = 7; - string name_en = 8; - string parent_tag_id = 9; - string created_by = 10; - string updated_by = 11; - string parent_tag_name = 12; // 列表展示用,由 parent_tag_id 解析 -} - -message ListAudioMaterialCategoriesReq { - bionode.common.v1.PageRequest page = 1; - optional string status = 2; // 未设置不筛选 - string name = 3; - string type = 4; - string code = 5; - string parent_tag_id = 6; -} - -message AudioMaterialCategoryListRes { - repeated AudioMaterialCategoryInfo categories = 1; - bionode.common.v1.PageResponse page = 2; -} - -message AudioMaterialCategoryRes { - AudioMaterialCategoryInfo category = 1; -} - -message CreateAudioMaterialCategoryReq { - string name = 1; - string type = 2; // 必填 - string code = 3; // 必填;与 type 组合唯一 - string name_en = 4; - string status = 5; // 默认「启用」 - string parent_tag_id = 6; - string created_by = 7; - string updated_by = 8; -} - -message UpdateAudioMaterialCategoryReq { - string id = 1; - optional string name = 2; - optional string status = 3; - optional string type = 4; - optional string code = 5; - optional string name_en = 6; - optional string parent_tag_id = 7; - optional string updated_by = 8; -} - -// 音频原料标签(覆盖 sleep_stage / content_form / mechanism / engineering 等形态) -message AudioMaterialTagValue { - string tag_id = 1; - string code = 2; - string name = 3; -} - -message AudioMaterialTag { - string tag_id = 1; - string code = 2; - string name = 3; - optional string en_name = 4; - optional string parent_tag_id = 5; - optional string parent_tag_code = 6; - optional AudioMaterialTagValue value = 7; - repeated double band_values = 8; - optional double relative_loudness = 9; -} - -message AudioMaterialInfo { - string id = 1; - string description = 2; - bool status = 3; // true=启用 false=禁用(与库 bool 一致) - string create_time = 4; - string update_time = 5; - string audio_name = 6; - string audio_url = 7; - int32 operation_type = 8; - string created_by = 9; - string updated_by = 10; - repeated AudioMaterialTag sleep_stage_tags = 11; - repeated AudioMaterialTag content_form_tags = 12; - repeated AudioMaterialTag mechanism_tags = 13; - repeated AudioMaterialTag audio_engineering_tags = 14; - repeated AudioMaterialTag medical_risk_tags = 15; - repeated AudioMaterialTag evidence_level_tags = 16; -} - -message ListAudioMaterialsReq { - bionode.common.v1.PageRequest page = 1; - optional bool status = 2; // 未设置不筛选;true/false 按启用态筛 - string name = 3; - repeated string tags = 4; // 按各类 *_tags 的 name/code 匹配 -} - -message AudioMaterialListRes { - repeated AudioMaterialInfo materials = 1; - bionode.common.v1.PageResponse page = 2; -} - -message AudioMaterialRes { - AudioMaterialInfo material = 1; -} - -message DistinctTagsRes { - repeated string tags = 1; -} - -message CreateAudioMaterialReq { - string description = 1; - string audio_name = 2; // 必填 - string audio_url = 3; - int32 operation_type = 4; - string created_by = 5; - string updated_by = 6; - repeated AudioMaterialTag sleep_stage_tags = 7; - repeated AudioMaterialTag content_form_tags = 8; - repeated AudioMaterialTag mechanism_tags = 9; - repeated AudioMaterialTag audio_engineering_tags = 10; - repeated AudioMaterialTag medical_risk_tags = 11; - repeated AudioMaterialTag evidence_level_tags = 12; - repeated double embedding = 13; -} - -message UpdateAudioMaterialReq { - string id = 1; // 必填(对应文档 _id) - optional string description = 2; - optional bool status = 3; // true=启用 false=禁用;未设置则不改 - optional string audio_name = 4; // 可选;设置时不能为空 - optional string audio_url = 5; - optional int32 operation_type = 6; - optional string created_by = 7; - optional string updated_by = 8; - repeated AudioMaterialTag sleep_stage_tags = 9; - repeated AudioMaterialTag content_form_tags = 10; - repeated AudioMaterialTag mechanism_tags = 11; - repeated AudioMaterialTag audio_engineering_tags = 12; - repeated AudioMaterialTag medical_risk_tags = 13; - repeated AudioMaterialTag evidence_level_tags = 14; - repeated double embedding = 15; -} - -message UpdateAudioMaterialStatusReq { - string id = 1; - bool status = 2; // true=启用 false=禁用 -} - -// ============================================================ -// Somni — 睡眠报告、方案、基线 -// ============================================================ - -message SomniSetAlarmReq { - string uid = 1; - string session_id = 2; - string alarm_time = 3; // HH:mm - int32 estimated_sleep_minutes = 4; // 默认 480 - string language = 5; // 语言标识(zh/en) -} - -message SomniSetAlarmRes { - string session_id = 1; - string alarm_id = 2; - string plan_id = 3; - repeated string mhr_codes = 4; // 人格编码(来自 quiz_results) -} - -message SomniGetPlanReq { - string uid = 1; - string session_id = 2; -} - -message SomniGetPlanRes { - string session_id = 1; - string plan_id = 2; - string title = 3; - string subtitle = 4; - bool is_started = 5; - string phases = 6; // JSON: phases 数组 -} - -message SomniMarkPlanStartedReq { - string uid = 1; - string session_id = 2; -} - -message SomniMarkPlanStartedRes { - bool ok = 1; - string msg = 2; - string phases = 3; // JSON - string record_date = 4; -} - -message SomniMarkPlanStoppedReq { - string uid = 1; - string session_id = 2; -} - -message SomniMarkPlanStoppedRes { - bool ok = 1; - string msg = 2; -} - -/** 开启伴睡时可选:仅提交有修改的阶段时长,未出现的阶段沿用库里的 duration_sec */ -message SomniPhaseDurationPatch { - string phase = 1; - int32 duration_sec = 2; -} - -message SomniStartSomniDeviceSessionReq { - string uid = 1; - string session_id = 2; - string app_instance_id = 3; - repeated SomniPhaseDurationPatch phase_duration_patches = 4; - string language = 5; // 用户语言偏好(zh / en) -} - -message SomniStartSomniDeviceSessionRes { - bool ok = 1; - string msg = 2; - string device_id = 3; -} - -message SomniStopSomniDeviceSessionReq { - string uid = 1; - string session_id = 2; -} - -message SomniStopSomniDeviceSessionRes { - bool ok = 1; - string msg = 2; -} - -/** 列表项:key 为 YYYY-MM-DD 中的「日」数字字符串(无前导零,如 7),val 为数值 */ -message SomniBaselineFusionKeyValInt32 { - string key = 1; - int32 val = 2; -} - -/** 列表项:key 同上;val 为是否有日程 */ -message SomniBaselineFusionKeyValBool { - string key = 1; - bool val = 2; -} - -/** 融合数据:14 天步数序列及 14 天总步数 */ -message SomniBaselineFusionSteps { - repeated SomniBaselineFusionKeyValInt32 list = 1; - int32 total_steps = 2; -} - -/** 融合数据:14 天是否当日有日历/日程记录 */ -message SomniBaselineFusionCalendar { - repeated SomniBaselineFusionKeyValBool list = 1; -} - -/** 融合数据:基准日单日预报(和风 daily 一条);无配置密钥或失败时各 string 为空 */ -message SomniBaselineFusionWeather { - string fx_date = 1; // 预报日期 - string temp_max = 2; // 最高温度 - string temp_min = 3; // 最低温度 - string icon = 4; // 天气图标(根据时间返回日间/夜间) - string precip = 5; // 降水量 mm - string uv_index = 6; // 紫外线指数 - string humidity = 7; // 相对湿度 % - string pressure = 8; // 气压 hPa - string vis = 9; // 能见度 km - string wind_dir = 10; // 风向(根据时间返回日间/夜间) - string wind_scale = 11; // 风力等级(根据时间返回日间/夜间) - string light_intensity = 12; // 光照强度(根据 UV 指数映射) - string aqi = 13; // 空气质量指数 - string aqi_category = 14; // 空气质量类别(根据 AQI 映射) - string sunrise = 15; // 日出时间 - string sunset = 16; // 日落时间 -} - -/** 融合数据:14 天分数序列 + 基准日当天分数 */ -message SomniBaselineFusionScore { - repeated SomniBaselineFusionKeyValInt32 list = 1; - int32 today = 2; -} - -/** GetBaseline 融合块:步数、日历、天气、分数(list 顺序与 date_strings 一致) */ -message SomniBaselineFusionDaily { - SomniBaselineFusionSteps steps = 1; - SomniBaselineFusionCalendar calendar = 2; - SomniBaselineFusionWeather weather = 3; - SomniBaselineFusionScore score = 4; -} - -message SomniGetFusionReq { - string uid = 1; - string date = 2; // YYYY-MM-DD,可选 - string language = 3; // 语言标识,默认 zh -} - -message SomniGetFusionRes { - string ai_insight = 1; // JSON - string schedules = 2; // JSON 数组 -} - -message SomniGetBaselineReq { - string uid = 1; - string date = 2; // YYYY-MM-DD,可选,默认当天 - string language = 3; // 语言标识,默认 zh - /** 和风 location:LocationID 或「经度,纬度」;空则服务端默认(如北京 101010100) */ - string location = 4; - /** /app/quiz/chat 完成态返回的 session_id;用于读取问卷 user_profile 缓存覆盖 avg 作息 */ - string session_id = 5; -} - -message SomniGetBaselineRes { - string ai_insight = 1; // JSON - string date_strings = 2; // JSON 数组,14 天日期 - string records = 3; // JSON 数组 - string schedules = 4; // JSON 数组 - SomniBaselineFusionDaily fusion_daily = 5; - /** 问卷缓存 usual_sleep_wake_time.sleep_time;空则 BFF 用 14 天记录计算 */ - string quiz_avg_bedtime = 6; - /** 问卷缓存 usual_sleep_wake_time.wake_time;空则 BFF 用 14 天记录计算 */ - string quiz_avg_wake_time = 7; -} - -message SomniGetReportReq { - string uid = 1; - string record_date = 2; // YYYY-MM-DD,可选则默认当天 - string language = 3; // 语言标识,默认 zh -} - -message SomniGetReportRes { - string record_date = 1; - string report = 2; // JSON: { main, notice, sleep_summary, pain_point_analysis, quality_analysis } -} - -// ----- 睡眠星球 / 梦境画廊(somni_dream_universe_assets) ----- - -message SomniSleepPlanetIllustrationItem { - string url = 1; - int32 sort_order = 2; -} - -message SomniSleepPlanetParams { - double planet_size = 1; // 星球大小(缩放系数),范围 0.6 ~ 1.2 - string land_color = 2; // 陆地颜色(hex,带 # 前缀) - string water_color = 3; // 水域颜色(hex,带 # 前缀) - string ring_color = 4; // 环颜色(hex,带 # 前缀) - double ring_radius = 5; // 光晕半径,范围 1.4 ~ 2.0 - double planet_noise_density = 6; // 纹理密度,范围 0.25 ~ 0.85 -} - -message SomniSleepPlanetStability { - int32 score = 1; - string grade_label = 2; -} - -message SomniSleepPlanetMetrics { - double deep_min = 1; // 深睡时长(分钟) - double deep_pct = 2; // 深睡占比(%) - SomniSleepPlanetStability efficiency = 3; // 睡眠效率(与 stability 同结构) - SomniSleepPlanetStability stability = 4; // 体征稳定性 -} - -message SomniSleepPlanetDetail { - string uid = 1; - string record_date = 2; - SomniSleepPlanetMetrics metrics = 3; - string title = 4; - string description = 5; - SomniSleepPlanetParams planet = 6; - repeated SomniSleepPlanetIllustrationItem illustrations = 7; -} - -message SomniGetSleepPlanetReq { - string uid = 1; - string record_date = 2; // YYYY-MM-DD,可选 -} - -message SomniGetSleepPlanetRes { - bool found = 1; - SomniSleepPlanetDetail detail = 2; -} - -message SomniListSleepPlanetsReq { - string uid = 1; - bionode.common.v1.PageRequest page = 2; -} - -message SomniListSleepPlanetsRes { - repeated SomniSleepPlanetDetail planets = 1; - bionode.common.v1.PageResponse page = 2; -} - -message SomniValidateMonitorSessionReq { - string uid = 1; - string session_id = 2; -} - -message SomniValidateMonitorSessionRes { - bool ok = 1; -} - -message SomniGetMonitorPayloadReq { - string uid = 1; - string session_id = 2; -} - -message SomniGetMonitorPayloadRes { - string payload = 1; // JSON: { type, ts, phases, physiological, environment, message } -} - -// ----- Quiz 匿名提交流程(Somni) ----- - -message SomniGetUidByMhrCodesReq { - string survey_code = 1; - repeated string mhr_codes = 2; -} - -message SomniGetUidByMhrCodesRes { - string uid = 1; -} - -message SomniCreateQuizSessionReq { - string uid = 1; - string record_date = 2; // YYYY-MM-DD - string quiz_result_id = 3; - string survey_code = 4; -} - -message SomniCreateQuizSessionRes { - string session_id = 1; -} - -message SomniUpsertSomniAppInstUserReq { - string app_instance_id = 1; - string uid = 2; -} - -message SomniUpsertSomniAppInstUserRes { - bool ok = 1; - string msg = 2; -} - -message SomniDeleteSomniAppInstUserReq { - string app_instance_id = 1; -} - -// ----- Somni Quiz AI 对话问卷(HTTP /app/quiz/chat 对应此 RPC) ----- - -message SomniQuizPendingOption { - string option_id = 1; - string option_text = 2; -} - -// 与 proto/somni_quiz.proto 中 ConfigItemConfig 对齐(composite 子项 slider 等) -message SomniQuizConfigItemConfig { - int32 min = 1; - int32 max = 2; - int32 step = 3; -} - -message SomniQuizPendingQuestionConfigItem { - int32 index = 1; - string label = 2; - string format = 3; - // composite 子项(与 somni_quiz PendingQuestionConfigItem 对齐) - string title = 4; - string input_type = 5; - string description = 6; - repeated SomniQuizPendingOption options = 7; - SomniQuizConfigItemConfig config = 8; -} - -message SomniQuizPendingQuestionConfig { - repeated SomniQuizPendingQuestionConfigItem items = 1; -} - -message SomniQuizPendingQuestion { - string question_id = 1; - string qid = 2; - string title = 3; - string input_type = 4; - repeated string tags = 5; - repeated SomniQuizPendingOption options = 6; - SomniQuizPendingQuestionConfig config = 7; -} - -message SomniQuizDirectAnswerIn { - string question_id = 1; - repeated string selected_options = 2; - string input_value = 3; -} - -// Somni ChatQuiz final_result.report_sections 片段(完成态睡眠报告等) -message SomniQuizReportSection { - string title = 1; - string content = 2; -} - -message SomniQuizChatReq { - string client_session_id = 1; - bool resume = 2; - string survey_code = 3; - string language = 4; - string message = 5; - SomniQuizDirectAnswerIn direct_answer = 6; - string app_instance_id = 7; - /** 仅新建 BioNode 会话时供 InitQuiz 使用;Somni ChatQuiz 不传 quiz_mode */ - string quiz_mode = 8; - /** 城市名称,InitQuiz 时传入 */ - string city_name = 9; - /** 经纬度,预留 */ - double longitude = 10; - double latitude = 11; -} - -message SomniQuizChatRes { - string client_session_id = 1; - string assistant_message = 2; - SomniQuizPendingQuestion pending_question = 3; - bool finalized = 4; - string session_id = 5; - string uid = 6; - bool chat_state = 7; - double progress_percent = 8; - repeated string mhr_codes = 9; - /** 与外部 Somni ChatQuiz 对齐:RECORDED | UPDATED | PARTIAL | NOT_RECORDED 等 */ - string answer_status_code = 10; - /** 完成态 final_result.report_sections(共情解读、问题归因等);未结束时为空 */ - repeated SomniQuizReportSection report_sections = 11; -} - -// ============================================================ -// InterventionConfig — 干预配置相关消息 -// ============================================================ - -message InterventionCondition { - string kind = 1; // phase_in | env_threshold | metric_threshold | presence_required - repeated string phases = 2; // kind=phase_in 时使用 - string field = 3; // kind=env_threshold/metric_threshold 时使用 - string operator = 4; // gt | gte | lt | lte | eq - double value = 5; // 阈值数值 - bool bool_value = 6; // kind=presence_required 时使用 -} - -message InterventionScenario { - string light_json = 1; // SchemeV1 Light JSON 字符串 - string sound_json = 2; // SchemeV1 Sound JSON 字符串 - string scent_json = 3; // SchemeV1 Scent JSON 字符串 - int32 duration_sec = 4; // 干预持续时长(秒) - int32 transition_sec = 5; // 光渐变过渡时长(秒) -} - -message InterventionConfigInfo { - string id = 1; - string code = 2; - string name = 3; - bool enabled = 4; - int32 priority = 5; - int32 cooldown_sec = 6; - string condition_logic = 7; // any | all - repeated InterventionCondition conditions = 8; - InterventionScenario scenario = 9; - int32 sort_order = 10; - string create_time = 11; - string update_time = 12; - bool show = 13; // 是否在控制台事件控制列表中显示 - string event_type = 14; // oneshot | continuous -} - -message ListInterventionConfigsReq {} - -message InterventionConfigListRes { - repeated InterventionConfigInfo list = 1; -} - -message UpsertInterventionConfigReq { - string id = 1; // 空则新建,非空则更新 - string code = 2; - string name = 3; - bool enabled = 4; - int32 priority = 5; - int32 cooldown_sec = 6; - string condition_logic = 7; - repeated InterventionCondition conditions = 8; - InterventionScenario scenario = 9; - int32 sort_order = 10; - bool show = 11; - string event_type = 12; // oneshot | continuous -} - -message InterventionConfigRes { - InterventionConfigInfo config = 1; -} - -message TriggerInterventionReq { - string session_id = 1; - string code = 2; -} - -message StopInterventionReq { - string session_id = 1; -} - -message ResetAiThoughtRehearsalStateReq { - string session_id = 1; -} - -message EmitDeviceLinkAiThoughtReq { - string device_id = 1; - bool online = 2; - string reason = 3; -} - -// ============================================================ -// Somni — 监控页查询接口 Message -// ============================================================ - -// ==================== GetPlanPhases ==================== - -message SomniGetPlanPhasesReq { - string session_id = 1; -} - -message SomniPlanPhaseItem { - string phase = 1; - string phase_name = 2; - int32 duration_minutes = 3; - string light_description = 4; - string sound_description = 5; - string scent_description = 6; - string temp_description = 7; // 温干预说明 -} - -message SomniGetPlanPhasesRes { - repeated SomniPlanPhaseItem phases = 1; -} - -// ==================== GetInterventionEvents ==================== - -message SomniGetInterventionEventsReq { - string session_id = 1; -} - -message SomniInterventionEventItem { - string time = 1; - string event_type = 2; - string type = 3; - string code = 4; - string trigger_cause = 5; - string action_taken = 6; - string result_summary = 7; - string related_event_id = 8; - string duration = 9; // HH:mm:ss 格式的事件持续时长 -} - -message SomniGetInterventionEventsRes { - repeated SomniInterventionEventItem events = 1; -} - -// ==================== GetMonitorReportData ==================== - -message SomniGetMonitorReportDataReq { - string session_id = 1; - /** "real"(默认)从 MongoDB 读真实数据;"fake" 从 Redis 读展示数据 */ - string data_source = 2; -} - -message SomniGenerateMonitorDphReportsReq { - string session_id = 1; -} - -message SomniGenerateMonitorDphReportsRes { - bool success = 1; - string error = 2; -} - -message PhysiologicalDataPoint { - string time = 1; - double heart_rate = 2; - double respiration_rate = 3; - double body_motion = 4; -} - -message EnvironmentDataPoint { - string time = 1; - double temperature = 2; - double humidity = 3; - double illuminance = 4; - double noise = 5; -} - -message InterventionSummary { - int32 total_count = 1; - repeated SomniInterventionEventItem events = 2; -} - -// ---- 人格亮点 & 异常分级(睡眠报告增强) ---- - -message PersonalityHighlight { - string tag = 1; - string title = 2; - string subhead = 3; - string details = 4; -} - -message RadarMetrics { - string time = 1; // 拐点时间(HH:mm,对应 physiological_series.time) - int32 T_resistance_sec = 2; // 从开始到拐点的秒数 - double HR_initial = 3; // 拐点前心率均值 - double HR_stable = 4; // 拐点后稳定心率 - double RR_initial = 5; - double RR_stable = 6; - double body_motion = 7; // 拐点时刻体动幅度(百分比 0-100) -} - -message RadarResult { - string status = 1; // MATCHED / ANOMALY / PERFECT - PersonalityHighlight highlight = 2; - RadarMetrics metrics = 3; -} - -message AnomalySegment { - int64 start_ts = 1; - int64 end_ts = 2; - string type = 3; // data_gap / presence_false - string label = 4; - string start_time = 5; // HH:mm,对应 physiological_series.time - string end_time = 6; -} - -message AnomalyStatus { - string status = 1; // MATCHED / ANOMALY / PERFECT(与 RadarResult.status 同步) - string level = 2; // normal / light / severe - string message = 3; - bool session_completed = 4; - repeated AnomalySegment anomaly_segments = 5; -} - -// ---- 报告底部卡片 & 方案概览 ---- - -message EnvironmentCardSeries { - repeated double series = 1; - double avg = 2; -} - -message ClimateCard { - EnvironmentCardSeries temperature = 1; - EnvironmentCardSeries humidity = 2; -} - -message EnvironmentCards { - ClimateCard climate = 1; - EnvironmentCardSeries ambient_light = 2; // 光照度 - EnvironmentCardSeries acoustic_masking = 3; // 噪声 - EnvironmentCardSeries air_conditioner = 4; // 空调目标温度(4 阶段) -} - -message PlanOverviewRgb { - int32 r = 1; - int32 g = 2; - int32 b = 3; -} - -message PlanOverviewScent { - string scent_icon = 1; - string type = 2; -} - -message PlanOverviewLightPhase { - string phase = 1; - repeated PlanOverviewRgb color_stops = 2; -} - -message PlanOverviews { - reserved 1; - reserved "light_color_stops"; - repeated PlanOverviewScent scent = 2; - repeated string sound = 3; - repeated PlanOverviewLightPhase light_phases = 4; -} - -message SomniQuizUserProfileSleepWakeTime { - string sleep_time = 1; // HH:mm - string wake_time = 2; -} - -/** 问卷完成态 user_profile(Redis somni:quiz:user_profile:session:{sessionId}) */ -message SomniQuizUserProfile { - string name = 1; - string daytime_event = 2; - string improvement_goal = 3; - SomniQuizUserProfileSleepWakeTime usual_sleep_wake_time = 4; -} - -message SomniGetMonitorReportDataRes { - repeated PhysiologicalDataPoint physiological_series = 1; - repeated EnvironmentDataPoint environment_series = 2; - InterventionSummary intervention_summary = 3; - RadarResult radar_result = 4; - AnomalyStatus anomaly_status = 5; - EnvironmentCards environment_cards = 6; - PlanOverviews plan_overviews = 7; - /** 问卷 user_profile;Redis 无缓存时不返回 */ - SomniQuizUserProfile user_profile = 8; -} - -// ==================== GetSessionContext ==================== - -message SomniGetSessionContextReq { - string session_id = 1; -} - -message SomniSessionContextPlanPhase { - string phase = 1; - string phase_name = 2; - int32 duration_sec = 3; - int32 transition_sec = 4; - int32 duration_minutes = 5; - string schemes_json = 6; // schemes 数组的 JSON 字符串,结构与人格 periods[].schemes 一致 - string description = 7; -} - -message SomniGetSessionContextRes { - repeated string mhr_codes = 1; - repeated SomniSessionContextPlanPhase plan_phases = 2; - string survey_code = 3; // somni_sessions.survey_code,控制台会话为 console -} - -message SomniConsoleStopDeviceSessionReq { - string session_id = 1; -} - -message SomniConsoleStopDeviceSessionRes { - bool ok = 1; - string msg = 2; -} - -// ============================================================ -// ChatMessage — 对话记录相关消息 -// ============================================================ - -message ChatMessageInfo { - string id = 1; - string conversation_id = 2; - string biz_type = 3; - int32 message_index = 4; - string role = 5; // system | user | assistant - string content_json = 6; // content 序列化为 JSON 字符串(Mixed 类型) - string meta_data_json = 7; // meta_data 序列化为 JSON 字符串(Mixed 类型) - string create_time = 8; -} - -message GetChatMessagesReq { - string conversation_id = 1; -} - -message ChatMessageListRes { - repeated ChatMessageInfo list = 1; -} - -message ChatConversationItem { - string conversation_id = 1; - string biz_type = 2; - int32 message_count = 3; - string first_message_time = 4; - string last_message_time = 5; -} - -message ListChatConversationsReq { - string biz_type = 1; - string start_time = 2; - string end_time = 3; - bionode.common.v1.PageRequest page = 4; -} - -message ListUserChatConversationsReq { - string uid = 1; - string biz_type = 2; - bionode.common.v1.PageRequest page = 3; -} - -message ChatConversationListRes { - repeated ChatConversationItem conversations = 1; - bionode.common.v1.PageResponse page = 2; -} - -// ============================================================ -// Somni — 临时方案相关消息 -// ============================================================ - -message SomniGetTempPlanReq { - repeated string mhr_codes = 1; - string language = 2; - string type = 3; // init | interv,默认 init -} - -message SomniGetTempPlanRes { - bool found = 1; - string title = 2; - string subtitle = 3; - string phases = 4; // JSON: phases 数组 -} - -// ============================================================ -// Somni 对话 — 音频上传 -// ============================================================ - -message UploadChatAudioReq { - bytes audio_data = 1; // 音频二进制数据(PCM 16kHz 16bit mono) - string prefix = 2; // 存储目录前缀,如 "somni/chat/audio" -} - -message UploadChatAudioRes { - string audio_url = 1; // CDN URL -} - -// ============================================================ -// Somni 对话 — 消息落库 -// ============================================================ - -message AppendChatMessageReq { - string conversation_id = 1; // 会话标识 - string biz_type = 2; // 业务类型(somni_chat) - string role = 3; // system / user / assistant - string content_json = 4; // content 序列化为 JSON 字符串 - string meta_data_json = 5; // meta_data 序列化为 JSON 字符串 -} - -// ============================================================ -// Somni — 睡眠地图 -// ============================================================ - -message SomniSleepMapRegion { - string province_code = 1; - string city_code = 2; - string district_code = 3; - string province = 4; // 展示名(随请求 language) - string city = 5; - string district = 6; - string full_path = 7; // 当前粒度完整路径(与 name 同语言) -} - -/** value_json / city_avg_json:JSON 编码后的数字或字符串,与 dimensions.value / city_avg 对齐 */ -message SomniSleepMapDimensionGrpc { - double score = 1; - double weight = 2; - string value_json = 3; - string city_avg_json = 4; - double city_score = 5; -} - -message SomniSleepMapDimensionsGrpc { - SomniSleepMapDimensionGrpc deep_sleep = 1; - SomniSleepMapDimensionGrpc sleep_duration = 2; - SomniSleepMapDimensionGrpc sleep_efficiency = 3; - SomniSleepMapDimensionGrpc abnormal_events = 4; - SomniSleepMapDimensionGrpc routine_regularity = 5; -} - -message SomniSleepMapAnalysisGrpc { - string aid = 1; // somni_sleep_analysis._id(hex) - string uid = 2; - string stats_date = 3; - SomniSleepMapRegion region = 4; - string user_name = 5; - double score = 6; - double sleep_seconds = 7; - double deep_sleep_seconds = 8; - double deep_sleep_ratio = 9; // 响应中为 0~100 整数百分比(相对 DB 0~1) - string evaluation = 10; - SomniSleepMapDimensionsGrpc dimensions = 11; - bool is_env_sensitive = 12; -} - -message SomniSleepMapDistrictGrpc { - SomniSleepMapRegion region = 1; - double score = 2; // 综合得分均值,整数(睡眠地图概况) - double sleep_seconds = 3; - double deep_sleep_ratio = 4; // 深睡占比,0~100 整数百分比(相对数据库中 0~1 小数) - double sensitive_user_ratio = 5; // 环境敏感人群占比,0~100 整数百分比 - string heatmap_url = 6; -} - -message SomniSleepMapCitySummaryGrpc { - SomniSleepMapRegion region = 1; - double avg_comprehensive_score = 2; // 全市下辖各区 score 算术均值,取整 - int32 score_beat_count = 3; // 同城同日得分低于当前用户的人数 - int32 deep_ratio_beat_count = 4; // 同城同日深睡占比(DB 0~1)低于当前用户的人数 -} - -message SomniGetSleepMapCityOverviewReq { - string uid = 1; - string stats_date = 2; - string language = 3; // zh | en,默认 zh - string province_code = 4; - string city_code = 5; - string district_code = 6; -} - -message SomniGetSleepMapCityOverviewRes { - string stats_date = 1; - bool found_focus_district = 2; - SomniSleepMapDistrictGrpc focus_district = 3; - SomniSleepMapCitySummaryGrpc city_summary = 4; -} - -message SomniGetSleepMapHighlightsReq { - string uid = 1; // 当前登录用户(鉴权) - string aid = 2; // somni_sleep_analysis._id(hex);空则按 uid + stats_date 查本人当日分析 - string stats_date = 3; - string language = 4; // zh | en,默认 zh -} - -message SomniGetSleepMapHighlightsRes { - bool found = 1; - SomniSleepMapAnalysisGrpc analysis = 2; - int32 city_rank = 3; - int32 beat_ratio = 4; // 超越占比,0~100 整数(百分比);同城全日排名中优于其他人所占比例 - bool is_self = 5; -} - -message SomniSleepMapLeaderboardRowGrpc { - int32 rank = 1; - string uid = 2; - string aid = 3; // 该行对应 somni_sleep_analysis._id(hex) - string user_name = 4; - double deep_sleep_seconds = 5; - double sleep_seconds = 6; - double score = 7; - bool is_self = 8; -} - -message SomniSleepMapCurrentUserSummaryGrpc { - double score = 1; - double deep_sleep_seconds = 2; - double sleep_seconds = 3; - string evaluation = 4; - int32 city_rank = 5; - string aid = 6; // 当前登录用户当日分析的 somni_sleep_analysis._id;无分析为空 -} - -message SomniGetSleepMapCityRankingReq { - string uid = 1; - string stats_date = 2; - int32 limit = 3; - string language = 4; // zh | en,默认 zh -} - -message SomniGetSleepMapCityRankingRes { - string stats_date = 1; - SomniSleepMapRegion filter_region = 2; - repeated SomniSleepMapLeaderboardRowGrpc leaderboard = 3; - SomniSleepMapCurrentUserSummaryGrpc current_user_summary = 4; -} - -// ============================================================ -// DPH 控制台专用 -// ============================================================ - -message CreateDphConsoleSessionReq { - string uid = 1; // fl_users uid(受试者 linked_accounts.uid) - string scale_uid = 2; // 量表用户 ID(溯源) - string record_date = 3; // 方案日期 YYYY-MM-DD - string phases_json = 4; // JSON 序列化的 phases 数组 -} - -message CreateDphConsoleSessionRes { - string session_id = 1; -} diff --git a/proto/bionode_common.proto b/proto/bionode_common.proto deleted file mode 100644 index 6b0f155..0000000 --- a/proto/bionode_common.proto +++ /dev/null @@ -1,100 +0,0 @@ -syntax = "proto3"; - -// 公共骨架 message — 被 4 个 bionode_*.proto 引用 -// -// 设计原则(与 docs/architecture/proto-style-guide.md 同源): -// 1. AIP-122 标准方法的入参/出参共用结构由本文件定义; -// 2. 所有持久化资源的审计字段统一用 Metadata; -// 3. 列表接口统一用 PageRequest / PageResponse; -// 4. 任何「成功 / 失败 + 提示」语义的简单返回统一用 OperationResponse; -// 5. 任何「N 个 ID」入参统一用 IdsRequest(禁止逗号分隔字符串)。 -// -// 关于版本:本文件仍属 v1,与现有 4 个业务 proto 同步演进。 -// 字段号管理规则:见 .cursor/rules/grpc-service.mdc(PR5 重写)。 -package bionode.common.v1; - -// 空消息。语义等价于 google.protobuf.Empty,但显式定义在 BioNode 包内 -// 便于未来按需扩展(如增加 trace_id / request_id 等公共字段)而不破坏现有 RPC。 -message Empty {} - -// 单 ID 请求 -message IdRequest { - // MongoDB ObjectId 字符串(24 字符 hex) - string id = 1; -} - -// N 个 ID 请求(取代旧 admin proto 的 IdsRequest{ string ids = 1; // 逗号分隔 }) -message IdsRequest { - repeated string ids = 1; -} - -// 列表分页 / 过滤入参 -// -// AIP-158 的简化版本:保留 page/page_size 而不切换 page_token, -// 因为 BioNode 前端早已用 page/page_size 形态,本次重构在 BFF 层做兼容映射, -// proto 内部按规范来即可。 -message PageRequest { - // 页码(从 1 开始,默认 1) - int32 page = 1; - // 每页条数(默认 10,最大 100) - int32 page_size = 2; - // 通用关键词(模糊搜索,可选) - string keyword = 3; - // 排序表达式:形如 "create_time desc" 或 "name asc" - // 解析规则在各 service 实现,无统一字段映射保证 - string order_by = 4; -} - -// 列表分页响应公共块 -message PageResponse { - // 总条数 - int32 total = 1; - // 当前页码(回显) - int32 page = 2; - // 当前页大小(回显) - int32 page_size = 3; - // 总页数 - int32 total_pages = 4; -} - -// 资源审计字段三件套 -// 所有持久化资源 Message 必须嵌入 Metadata metadata = N; 字段(PR4 起强制) -message Metadata { - // 业务状态:1=启用,0=禁用,其余值由各业务自定义 - int32 status = 1; - // 创建时间,ISO 8601 UTC(如 2026-04-02T15:30:00.000Z) - string create_time = 2; - // 更新时间,ISO 8601 UTC - string update_time = 3; -} - -// 通用「成功/失败 + 提示」响应 -// -// 取代 device-gateway proto 中各 *Res 重复的 { bool ok = 1; string msg = 2; } 模式。 -// 不要滥用:有具体业务返回值的 RPC 仍应定义专属 Response。 -message OperationResponse { - bool ok = 1; - string msg = 2; -} - -// 选项树(admin 用于下拉、级联) -message OptionItem { - string value = 1; - string label = 2; - repeated OptionItem children = 3; -} - -// 业务错误详情(可选作为 Response 字段携带) -// -// 注意:本 message 不强制使用。 -// gRPC 错误首选仍走 throw BusinessException → GrpcExceptionFilter → -// gRPC trailing metadata(com.google.rpc.Status)。本 Error 仅用于 -// 「成功调用但部分子项失败」的批处理场景。 -message Error { - // 业务错误码(与 libs/common/src/exceptions/error-code.enum.ts 对齐) - string code = 1; - // 用户可读错误信息 - string message = 2; - // 详情(JSON 字符串),如校验失败的字段列表 - string details_json = 3; -} diff --git a/proto/uburnode.proto b/proto/uburnode.proto new file mode 100644 index 0000000..bb8b340 --- /dev/null +++ b/proto/uburnode.proto @@ -0,0 +1,128 @@ +syntax = "proto3"; + +// 功能手板对外契约(对齐 HTTP /api/audio;问卷 GetAnswer 本期可 stub) +package uburnode.v1; + +import "google/protobuf/struct.proto"; + +message IdRequest { + string id = 1; +} + +message OperationResponse { + bool ok = 1; + string msg = 2; +} + +message SomniTagRef { + optional string tag_id = 1; + optional string code = 2; + optional string name = 3; +} + +message ContentFormTag { + optional string tag_id = 1; + optional string code = 2; + optional string name = 3; + optional string en_name = 4; + optional string parent_tag_id = 5; + optional string parent_tag_code = 6; +} + +message AudioEngineeringTag { + optional string tag_id = 1; + optional string code = 2; + optional string name = 3; + optional SomniTagRef value = 4; + repeated double band_values = 5; + optional double relative_loudness = 6; +} + +message CreateAudioReq { + string audio_name = 1; + optional string audio_url = 2; + optional string description = 3; + optional int32 operation_type = 4; + optional string created_by = 5; + optional string updated_by = 6; + optional bool status = 7; + repeated SomniTagRef sleep_stage_tags = 8; + repeated ContentFormTag content_form_tags = 9; + repeated SomniTagRef mechanism_tags = 10; + repeated AudioEngineeringTag audio_engineering_tags = 11; + repeated SomniTagRef medical_risk_tags = 12; + repeated SomniTagRef evidence_level_tags = 13; + repeated double embedding = 14; +} + +message UpdateAudioReq { + string material_id = 1; + optional string audio_name = 2; + optional string audio_url = 3; + optional string description = 4; + optional int32 operation_type = 5; + optional string created_by = 6; + optional string updated_by = 7; + optional bool status = 8; + repeated SomniTagRef sleep_stage_tags = 9; + repeated ContentFormTag content_form_tags = 10; + repeated SomniTagRef mechanism_tags = 11; + repeated AudioEngineeringTag audio_engineering_tags = 12; + repeated SomniTagRef medical_risk_tags = 13; + repeated SomniTagRef evidence_level_tags = 14; + repeated double embedding = 15; +} + +message SearchAudioReq { + optional string query_text = 1; + repeated string sleep_stage_tags = 2; + repeated string content_tags = 3; + repeated string disliked_tags = 4; + optional int32 top_k = 5; +} + +message AudioMaterial { + string id = 1; + string description = 2; + bool status = 3; + string create_time = 4; + string update_time = 5; + string audio_name = 6; + string audio_url = 7; + int32 operation_type = 8; + string created_by = 9; + string updated_by = 10; + repeated SomniTagRef sleep_stage_tags = 11; + repeated ContentFormTag content_form_tags = 12; + repeated SomniTagRef mechanism_tags = 13; + repeated AudioEngineeringTag audio_engineering_tags = 14; + repeated SomniTagRef medical_risk_tags = 15; + repeated SomniTagRef evidence_level_tags = 16; + repeated double embedding = 17; +} + +message AudioMaterialRes { + AudioMaterial material = 1; +} + +message SearchAudioRes { + repeated google.protobuf.Struct materials = 1; +} + +service AudioService { + rpc CreateAudio (CreateAudioReq) returns (AudioMaterialRes); + rpc UpdateAudio (UpdateAudioReq) returns (OperationResponse); + rpc DeleteAudio (IdRequest) returns (OperationResponse); + rpc SearchAudio (SearchAudioReq) returns (SearchAudioRes); +} + +message GetAnswerReq { + string uid = 1; + string answer_id = 2; +} + +message GetAnswerRes {} + +service QuizService { + rpc GetAnswer (GetAnswerReq) returns (GetAnswerRes); +} diff --git a/proto/uburnode_somni.proto b/proto/uburnode_somni.proto new file mode 100644 index 0000000..edcfbbc --- /dev/null +++ b/proto/uburnode_somni.proto @@ -0,0 +1,211 @@ +syntax = "proto3"; + +// 量产对外契约(见 docs/量产中间件接口文档.md) +// Quiz / Report / Audio +package uburnode.somni.v1; + +import "google/protobuf/struct.proto"; + +message GetAnswerReq { + string uid = 1; // 必传 + string answer_id = 2; // 必传:答卷 _id +} + +// answers 为整份答卷数组的 JSON 文本(与库 answers 同形),客户端一次 JSON.parse。 +message GetAnswerRes { + string answers = 1; +} + +service QuizService { + rpc GetAnswer (GetAnswerReq) returns (GetAnswerRes); +} + +message ReportDateReq { + string uid = 1; // 必传 + string record_date = 2; // 必传:记录日期 +} + +message SleepSummary { + int32 body_battery = 1; + string body_battery_status = 2; + int32 total_minutes = 3; + int32 deep_sleep_minutes = 4; + int32 avg_heart_rate = 5; + int32 avg_respiratory_rate = 6; +} + +message GetSummaryRes { + SleepSummary sleep_summary = 1; +} + +message SleepEventDetail { + string event_type = 1; + string duration = 2; + string trigger_cause = 3; + string action_taken = 4; + string result_summary = 5; +} + +message Intervention { + string type = 1; + string event_time = 2; + string event_type = 3; + string duration = 4; + string trigger_cause = 5; + string action_taken = 6; + string result_summary = 7; +} + +message SleepEventItem { + string event_time = 1; + string type = 2; + string code = 3; + repeated SleepEventDetail events = 4; + Intervention intervention = 5; +} + +message IdfStage { + string stage = 1; + string start = 2; + string end = 3; +} + +message PhysioMetrics { + int32 heart_rate = 1; + int32 respiration_rate = 2; +} + +message PhysioDataPoint { + string collected_at = 1; + PhysioMetrics metrics = 2; +} + +message EnvDataPoint { + string collected_at = 1; + int32 temperature = 2; + int32 humidity = 3; + int32 illuminance = 4; + int32 noise = 5; +} + +message GetEventsRes { + string record_date = 1; + repeated SleepEventItem sleep_events = 2; + int32 event_count = 3; + int32 abnormal_count = 4; + int32 intervention_count = 5; + repeated IdfStage idf_data = 6; + repeated PhysioDataPoint physio_data = 7; + repeated EnvDataPoint env_data = 8; +} + +message EnvMetric { + int32 value = 1; + int32 min = 2; + int32 max = 3; +} + +message EnvironmentSummary { + EnvMetric temperature = 1; + EnvMetric humidity = 2; + EnvMetric illuminance = 3; + EnvMetric noise = 4; +} + +message GetEnvironmentRes { + EnvironmentSummary environment_summary = 1; +} + +message SleepStagePart { + int32 minutes = 1; + int32 percent = 2; +} + +message SleepStructure { + SleepStagePart awake = 1; + SleepStagePart rem_sleep = 2; + SleepStagePart light_sleep = 3; + SleepStagePart deep_sleep = 4; +} + +message GetStructureRes { + SleepStructure sleep_structure = 1; +} + +message SleepQuality { + int32 time_in_bed_minutes = 1; + int32 sleep_onset_latency_minutes = 2; + int32 sleep_efficiency = 3; + string bedtime = 4; + string wake_up_time = 5; + int32 awake_after_onset_minutes = 6; +} + +message GetSleepQualityRes { + SleepQuality sleep_quality = 1; +} + +service ReportService { + rpc GetSummary (ReportDateReq) returns (GetSummaryRes); + rpc GetEvents (ReportDateReq) returns (GetEventsRes); + rpc GetEnvironment (ReportDateReq) returns (GetEnvironmentRes); + rpc GetStructure (ReportDateReq) returns (GetStructureRes); + rpc GetSleepQuality (ReportDateReq) returns (GetSleepQualityRes); +} + +message GetAudioReq { + optional int32 page = 1; // 页码,≥ 1;fetch_all 时忽略 + optional int32 page_size = 2; // 每页条数 + optional bool fetch_all = 3; // true 时分页字段忽略,拉全量 + optional string query_text = 4; // 搜索词,如「雨声」;空则不过滤 + optional string tag_code = 5; // 内容形态标签 code;空则不按标签过滤 +} + +message AudioListItem { + optional string id = 1; + optional string audio_name = 2; + optional string audio_url = 3; + optional string cover_url = 4; + optional string description = 5; + optional int32 vip = 6; // 库无该字段时显式返回 0 +} + +message GetAudioRes { + repeated AudioListItem list = 1; + int32 page = 2; + int32 page_size = 3; + int32 total = 4; +} + +message GetAudioTagReq {} + +message TagDictItem { + string type = 1; + string code = 2; + string name = 3; + string name_en = 4; + string id = 5; + google.protobuf.Value parent_tag_id = 6; // 库为 null/缺省时显式返回 null + string status = 7; +} + +message GetAudioTagRes { + repeated TagDictItem tags = 1; +} + +message GetHotReq {} + +message HotKeyword { + string keyword = 1; + int64 score = 2; +} + +message GetHotRes { + repeated HotKeyword items = 1; +} + +service AudioService { + rpc GetAudio (GetAudioReq) returns (GetAudioRes); + rpc GetAudioTag (GetAudioTagReq) returns (GetAudioTagRes); + rpc GetHot (GetHotReq) returns (GetHotRes); +} diff --git a/pyproject.toml b/pyproject.toml index 0d28197..c12ebcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "elasticsearch>=8.15.0,<9", "aiohttp>=3.11.0", "grpcio>=1.68.0", + "grpcio-reflection>=1.68.0", "onnxruntime>=1.19.0", "transformers>=4.40.0", "numpy>=1.26.0", @@ -47,6 +48,9 @@ testpaths = ["tests"] [tool.ruff] line-length = 100 target-version = "py312" +extend-exclude = [ + "app/uburnode_grpc/grpc_gen", +] [tool.ruff.lint] select = ["E", "F", "I", "UP"] diff --git a/scripts/gen_proto.sh b/scripts/gen_proto.sh deleted file mode 100755 index e2a38a4..0000000 --- a/scripts/gen_proto.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# 由 proto/bionode_comm.proto(及依赖 bionode_common.proto)生成 gRPC stub 到 app/bionode_grpc_clients/comm/grpc_gen/ -set -euo pipefail - -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -PROTO_DIR="$ROOT/proto" -OUT_DIR="$ROOT/app/bionode_grpc_clients/comm/grpc_gen" - -mkdir -p "$OUT_DIR" -touch "$OUT_DIR/__init__.py" - -python -m grpc_tools.protoc \ - -I"$PROTO_DIR" \ - --python_out="$OUT_DIR" \ - --grpc_python_out="$OUT_DIR" \ - "$PROTO_DIR/bionode_common.proto" \ - "$PROTO_DIR/bionode_comm.proto" - -# grpcio 生成物使用绝对 import,修正为包内相对 import -if [[ "$(uname)" == "Darwin" ]]; then - sed -i '' 's/^import bionode_common_pb2/from . import bionode_common_pb2/' "$OUT_DIR/bionode_comm_pb2.py" 2>/dev/null || true - sed -i '' 's/^import bionode_comm_pb2/from . import bionode_comm_pb2/' "$OUT_DIR/bionode_comm_pb2_grpc.py" 2>/dev/null || true -else - sed -i 's/^import bionode_common_pb2/from . import bionode_common_pb2/' "$OUT_DIR/bionode_comm_pb2.py" 2>/dev/null || true - sed -i 's/^import bionode_comm_pb2/from . import bionode_comm_pb2/' "$OUT_DIR/bionode_comm_pb2_grpc.py" 2>/dev/null || true -fi - -echo "gRPC stub generated at $OUT_DIR" diff --git a/scripts/gen_uburnode_proto.sh b/scripts/gen_uburnode_proto.sh new file mode 100755 index 0000000..4f36086 --- /dev/null +++ b/scripts/gen_uburnode_proto.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# 由 proto/uburnode.proto、proto/uburnode_somni.proto 生成 stub → app/uburnode_grpc/grpc_gen/ +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +PROTO_DIR="$ROOT/proto" +OUT_DIR="$ROOT/app/uburnode_grpc/grpc_gen" + +mkdir -p "$OUT_DIR" +touch "$ROOT/app/uburnode_grpc/__init__.py" +touch "$OUT_DIR/__init__.py" + +# 清理旧分拆 stub,避免混用 +rm -f "$OUT_DIR"/uburnode_audio_pb2*.py \ + "$OUT_DIR"/uburnode_quiz_pb2*.py \ + "$OUT_DIR"/bionode_common_pb2*.py + +python -m grpc_tools.protoc \ + -I"$PROTO_DIR" \ + --python_out="$OUT_DIR" \ + --grpc_python_out="$OUT_DIR" \ + "$PROTO_DIR/uburnode.proto" \ + "$PROTO_DIR/uburnode_somni.proto" + +if [[ "$(uname)" == "Darwin" ]]; then + SED_INPLACE=(sed -i '') +else + SED_INPLACE=(sed -i) +fi + +"${SED_INPLACE[@]}" \ + 's/^import uburnode_pb2 as uburnode__pb2/from . import uburnode_pb2 as uburnode__pb2/' \ + "$OUT_DIR/uburnode_pb2_grpc.py" +"${SED_INPLACE[@]}" \ + 's/^import uburnode_somni_pb2 as uburnode__somni__pb2/from . import uburnode_somni_pb2 as uburnode__somni__pb2/' \ + "$OUT_DIR/uburnode_somni_pb2_grpc.py" + +echo "UburNode gRPC stub generated at $OUT_DIR" +ls -la "$OUT_DIR" diff --git a/scripts/setup_github_secrets.sh b/scripts/setup_github_secrets.sh index f343e8c..6c83a17 100755 --- a/scripts/setup_github_secrets.sh +++ b/scripts/setup_github_secrets.sh @@ -85,7 +85,7 @@ main() { log "" log "=== 请在服务器首次克隆并配置 .env ===" log " git clone -b dev https://github.com/$REPO.git $DEPLOY_DIR" - log " cp $DEPLOY_DIR/.env.example $DEPLOY_DIR/.env # 编辑 COMM_GRPC_* 等" + log " cp $DEPLOY_DIR/.env.example $DEPLOY_DIR/.env # 编辑 MONGO_* / SOMNI_* / ES_* 等" log " cd $DEPLOY_DIR && docker compose up -d --build" log " ES_NODE 在 compose 中已覆盖为 http://elasticsearch:9200" log "" diff --git a/scripts/sync_es_from_comm.py b/scripts/sync_es_from_comm.py index a160799..460f92a 100644 --- a/scripts/sync_es_from_comm.py +++ b/scripts/sync_es_from_comm.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -"""Mongo Somni 集合 → ES 差异同步(单文件:适配、备份、对账、向量化、定时调度)。 +"""Mongo Somni 集合 → ES 全量重建同步(单文件:适配、备份、清空、向量化、定时调度)。 -以 Mongo _id 为准,只读不写源库: - - ES 有、Mongo 无 → 删 ES - - Mongo 有 → 比差异,有变才 upsert - - 先同步 somni_audio_tag_dictionary(name/name_en 向量),再同步 somni_audio_materials +以 Mongo `_id` 为准,只读不写源库: + - 先删除目标 ES 索引全部数据(删索引再 ensure 重建) + - 再按 Mongo 启用文档全量插入;ES 文档 id = Mongo `_id`,_source 不含 `id`/`_id` + - 先同步 somni_audio_tag_dictionary,再同步 somni_audio_materials 服务启动后按 SYNC_INTERVAL_DAYS 注册定时任务;也可手动执行本脚本。 @@ -45,7 +45,7 @@ build_material_description_text, material_source_for_es, ) -from app.mongo.materials import bson_to_jsonable # noqa: E402 +from app.core.bson_util import bson_to_jsonable # noqa: E402 # 供测试与外部脚本复用 __all__ = ( @@ -91,53 +91,49 @@ def mongo_doc_id(doc: dict[str, Any]) -> str: def material_doc_to_es(doc: dict[str, Any]) -> dict[str, Any] | None: - """Mongo 原料文档 → ES 文档(去掉 _id);无 _id 或无 audio_url 则跳过。""" + """Mongo 原料文档 → ES _source(去掉 _id/id);无 _id 或无 audio_url 则跳过。""" doc_id = mongo_doc_id(doc) if not doc_id: return None - return material_source_for_es(bson_to_jsonable(doc)) - - -def tag_dictionary_compare_snapshot(doc: dict[str, Any]) -> dict[str, Any]: - """标签词典 diff 快照(不含向量)。""" - keys = ( - "type", - "code", - "status", - "name", - "name_en", - "description", - "applicability", - "parent_tag_id", - "created_at", - "updated_at", - "created_by", - "updated_by", - ) - return {k: doc.get(k) for k in keys} - - -def material_compare_snapshot(doc: dict[str, Any]) -> dict[str, Any]: - """原料 diff 快照(不比较 dense vector,避免浮点噪声导致反复更新)。""" - snapshot = bson_to_jsonable(doc) - snapshot.pop("_id", None) - snapshot.pop("id", None) - snapshot.pop("description_vector", None) - return snapshot - - -def tag_documents_differ(desired: dict[str, Any], existing: dict[str, Any]) -> bool: - return tag_dictionary_compare_snapshot(desired) != tag_dictionary_compare_snapshot(existing) + payload = material_source_for_es(bson_to_jsonable(doc)) + if payload is None: + return None + payload.pop("_id", None) + payload.pop("id", None) + return payload -def material_documents_differ(desired: dict[str, Any], existing: dict[str, Any]) -> bool: - return material_compare_snapshot(desired) != material_compare_snapshot(existing) +def tag_doc_to_es(doc: dict[str, Any]) -> dict[str, Any] | None: + """Mongo 标签文档 → ES _source(去掉 _id/id);无 _id 则跳过。""" + doc_id = mongo_doc_id(doc) + if not doc_id: + return None + payload = bson_to_jsonable(doc) + payload.pop("_id", None) + payload.pop("id", None) + return payload def zero_vector(dim: int) -> list[float]: return [0.0] * dim +async def wipe_and_recreate_index( + es_client: AsyncElasticsearch, + es_search: EsSearch, + index: str, +) -> int: + """删除索引内全部数据:统计文档数 → 删索引 → ensure 重建映射。""" + count = 0 + if await es_client.indices.exists(index=index): + count_resp = await es_client.count(index=index) + count = int(count_resp.get("count", 0)) + await es_client.indices.delete(index=index) + logger.info("已删除 ES 索引以全量重建:{},原文档数={}", index, count) + await es_search.ensure_indices() + return count + + def write_backup(path: Path, records: list[dict[str, Any]]) -> None: if path.is_file(): path.unlink() @@ -226,39 +222,33 @@ async def run(self, *, dry_run: bool) -> dict[str, int]: if not dry_run: write_backup(self._settings.sync_tag_dictionary_backup_path, bson_to_jsonable(docs)) - source_ids = {mongo_doc_id(d) for d in docs if mongo_doc_id(d)} - es_ids = await self._es_search.list_all_tag_dictionary_doc_ids() - - for doc_id in es_ids - source_ids: - if dry_run: - stats["deleted"] += 1 - continue - try: - await self._client.delete(index=self._es_search.tag_dictionary_index, id=doc_id) - stats["deleted"] += 1 - except Exception as exc: + payloads: dict[str, dict[str, Any]] = {} + for doc in docs: + es_doc = tag_doc_to_es(doc) + if es_doc is None: stats["failed"] += 1 - logger.error("删除 ES 孤儿标签失败,id={},原因:{}", doc_id, exc) + continue + payloads[mongo_doc_id(doc)] = es_doc - for doc in docs: - outcome = await self._sync_one(doc, dry_run=dry_run) + if dry_run: + es_ids = await self._es_search.list_all_tag_dictionary_doc_ids() + stats["deleted"] = len(es_ids) + stats["created"] = len(payloads) + return stats + + stats["deleted"] = await wipe_and_recreate_index( + self._client, + self._es_search, + self._es_search.tag_dictionary_index, + ) + for doc_id, es_doc in payloads.items(): + outcome = await self._insert_one(doc_id, es_doc) stats[outcome] += 1 - if not dry_run: - self._es_search.clear_content_tag_vectors_cache() + self._es_search.clear_content_tag_vectors_cache() return stats - async def _sync_one(self, doc: dict[str, Any], *, dry_run: bool) -> str: - doc_id = mongo_doc_id(doc) - if not doc_id: - return "failed" - es_doc = bson_to_jsonable(doc) - es_doc.pop("_id", None) - existing = await self._es_search.get_tag_dictionary_source(doc_id) - if existing and not tag_documents_differ(es_doc, existing): - return "unchanged" - if dry_run: - return "created" if existing is None else "updated" + async def _insert_one(self, doc_id: str, es_doc: dict[str, Any]) -> str: try: es_doc["name_vector"] = await self._encoder.encode_one(str(es_doc.get("name", ""))) name_en = str(es_doc.get("name_en", "")).strip() @@ -270,7 +260,7 @@ async def _sync_one(self, doc: dict[str, Any], *, dry_run: bool) -> str: id=doc_id, document=es_doc, ) - return "created" if existing is None else "updated" + return "created" except Exception as exc: logger.error( "同步标签词典失败,id={},name={},原因:{}", @@ -321,40 +311,34 @@ async def run(self, *, dry_run: bool) -> dict[str, int]: continue payloads[mongo_doc_id(doc)] = es_doc - es_ids = await self._es_search.list_all_audio_doc_ids() - for doc_id in es_ids - set(payloads.keys()): - if dry_run: - stats["deleted"] += 1 - continue - try: - await self._client.delete(index=self._es_search.audio_index, id=doc_id) - stats["deleted"] += 1 - except Exception as exc: - stats["failed"] += 1 - logger.error("删除 ES 孤儿原料失败,id={},原因:{}", doc_id, exc) + if dry_run: + es_ids = await self._es_search.list_all_audio_doc_ids() + stats["deleted"] = len(es_ids) + stats["created"] = len(payloads) + return stats + stats["deleted"] = await wipe_and_recreate_index( + self._client, + self._es_search, + self._es_search.audio_index, + ) for doc_id, es_doc in payloads.items(): - outcome = await self._sync_one(doc_id, es_doc, dry_run=dry_run) + outcome = await self._insert_one(doc_id, es_doc) stats[outcome] += 1 return stats - async def _sync_one(self, doc_id: str, es_doc: dict[str, Any], *, dry_run: bool) -> str: - existing = await self._es_search.get_audio_source(doc_id) - if ( - existing - and not material_documents_differ(es_doc, existing) - and existing.get("description_vector") - ): - return "unchanged" - if dry_run: - return "created" if existing is None else "updated" + async def _insert_one(self, doc_id: str, es_doc: dict[str, Any]) -> str: try: es_doc["description_vector"] = await self._encoder.encode_one( str(es_doc.get("description_text", "")) ) - await self._client.index(index=self._es_search.audio_index, id=doc_id, document=es_doc) - return "created" if existing is None else "updated" + await self._client.index( + index=self._es_search.audio_index, + id=doc_id, + document=es_doc, + ) + return "created" except Exception as exc: logger.error( "同步原料失败,id={},name={},原因:{}", @@ -383,7 +367,7 @@ def __init__( self._settings = settings async def run(self, *, dry_run: bool = False) -> SyncJobResult: - logger.info("开始 Mongo → ES 差异同步,dry_run={}", dry_run) + logger.info("开始 Mongo → ES 全量重建同步,dry_run={}", dry_run) await self._es_search.migrate_legacy_indices() await self._es_search.ensure_indices() @@ -417,20 +401,16 @@ async def run(self, *, dry_run: bool = False) -> SyncJobResult: material_failed=material_stats["failed"], ) logger.info( - "Mongo → ES 同步结束:标签 拉取={} 删={} 增={} 改={} 未变={} 失败={};" - "原料 拉取={} 跳过={} 删={} 增={} 改={} 未变={} 失败={} dry_run={}", + "Mongo → ES 全量同步结束:标签 拉取={} 清索引={} 增={} 失败={};" + "原料 拉取={} 跳过={} 清索引={} 增={} 失败={} dry_run={}", result.tag_fetched, result.tag_deleted, result.tag_created, - result.tag_updated, - result.tag_unchanged, result.tag_failed, result.material_fetched, result.material_skipped, result.material_deleted, result.material_created, - result.material_updated, - result.material_unchanged, result.material_failed, dry_run, ) @@ -482,7 +462,7 @@ def start_sync_scheduler(state: AppState, settings: Settings) -> None: return async def _job() -> None: - logger.info("定时任务触发:Mongo → ES 差异同步") + logger.info("定时任务触发:Mongo → ES 全量重建同步") await run_scheduled_sync(state, settings) _scheduler = AsyncIOScheduler(timezone=UTC) @@ -547,8 +527,8 @@ async def _load_stage(stage: str) -> list: def main() -> None: - parser = argparse.ArgumentParser(description="Mongo Somni 集合差异同步至 ES") - parser.add_argument("--dry-run", action="store_true", help="只拉取比对,不写 ES、不备份") + parser = argparse.ArgumentParser(description="Mongo Somni 集合全量重建同步至 ES") + parser.add_argument("--dry-run", action="store_true", help="只拉取统计,不删 ES、不写 ES、不备份") args = parser.parse_args() exit_code = asyncio.run(_run_cli(dry_run=args.dry_run)) if exit_code != 0: diff --git a/scripts/test_grpc_connect.py b/scripts/test_grpc_connect.py deleted file mode 100755 index 5b9675c..0000000 --- a/scripts/test_grpc_connect.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -"""探测 comm-service gRPC 连通性(读取项目根 .env 的 COMM_GRPC_*)。 - -用法(须先激活虚拟环境或直接用 .venv 解释器): - source .venv/bin/activate && python scripts/test_grpc_connect.py - .venv/bin/python scripts/test_grpc_connect.py --timeout 15 -""" - -from __future__ import annotations - -import argparse -import asyncio -import logging -import sys -from pathlib import Path - -# 允许直接 python scripts/test_grpc_connect.py(无需手动 PYTHONPATH) -_ROOT = Path(__file__).resolve().parents[1] -if str(_ROOT) not in sys.path: - sys.path.insert(0, str(_ROOT)) - -logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") -_log = logging.getLogger("test_grpc_connect") - - -def _ensure_project_deps() -> None: - try: - import grpc # noqa: F401 - except ImportError: - _log.error("未找到项目依赖,请先: source .venv/bin/activate && pip install -e .") - raise SystemExit(3) - - -async def run_probe(timeout_sec: float) -> int: - from app.bionode_grpc_clients import CommClient - from app.core.config import get_settings - - settings = get_settings() - target = settings.comm_grpc_target - _log.info( - "comm-service 目标:%s(TLS=%s)", - target, - settings.comm_grpc_use_tls, - ) - - client = CommClient(settings) - try: - await client.connect() - tag_count = await client.ping(timeout_sec=timeout_sec) - except asyncio.TimeoutError: - _log.error("gRPC 通道在 %ss 内未就绪(网络不可达或端口未放行)", timeout_sec) - return 2 - except Exception as exc: - _log.error("gRPC 调用失败:%s", exc) - return 1 - finally: - await client.close() - - _log.info("连通成功,GetDistinctTags 返回 %s 个标签", tag_count) - return 0 - - -def main() -> None: - _ensure_project_deps() - parser = argparse.ArgumentParser(description="探测 comm-service gRPC") - parser.add_argument("--timeout", type=float, default=10.0, help="通道就绪与 RPC 超时(秒)") - args = parser.parse_args() - raise SystemExit(asyncio.run(run_probe(args.timeout))) - - -if __name__ == "__main__": - main() diff --git a/tests/test_audio_service.py b/tests/test_audio_service.py index 8a213e2..47beedb 100644 --- a/tests/test_audio_service.py +++ b/tests/test_audio_service.py @@ -1,4 +1,4 @@ -"""AudioService 创建/更新走 comm gRPC + ES 编排测试。""" +"""AudioService:直连 Mongo + ES 编排测试。""" from __future__ import annotations @@ -6,13 +6,12 @@ import pytest -from app.bionode_grpc_clients.comm.grpc_gen import bionode_comm_pb2 from app.schemas.audio import ( CreateAudioRequest, SearchAudioRequest, UpdateAudioRequest, ) -from app.services.audio import AudioService +from app.server.handboard.audio.service import AudioService def _service( @@ -21,33 +20,35 @@ def _service( es_sync: MagicMock | None = None, retrieval: MagicMock | None = None, search_cache: MagicMock | None = None, - comm: MagicMock | None = None, ) -> AudioService: retrieval_svc = retrieval or MagicMock() if retrieval is None: retrieval_svc.clear_sleep_stage_cache = AsyncMock() retrieval_svc.warm_sleep_stage_cache = AsyncMock() return AudioService( - comm or MagicMock(), + materials, es_sync or MagicMock(), retrieval_svc, - materials=materials, search_cache=search_cache, sleep_stage_rewarm_delay_sec=0, ) @pytest.mark.asyncio -async def test_create_audio_calls_grpc_then_es() -> None: - created = bionode_comm_pb2.AudioMaterialInfo(id="abc123", audio_name="雨声") - comm = MagicMock() - comm.create_audio_material = AsyncMock() - comm.list_audio_materials_by_name = AsyncMock(return_value=[created]) +async def test_create_audio_writes_mongo_then_es() -> None: + materials = MagicMock() + materials.insert_material = AsyncMock( + return_value={ + "id": "abc123", + "audio_name": "雨声", + "audio_url": "https://cdn.example.com/a.mp3", + } + ) es_sync = MagicMock() es_sync.upsert_somni_material = AsyncMock() search_cache = MagicMock() search_cache.clear_all = AsyncMock() - service = _service(comm=comm, es_sync=es_sync, search_cache=search_cache) + service = _service(materials=materials, es_sync=es_sync, search_cache=search_cache) result = await service.create_audio( CreateAudioRequest.model_validate( @@ -56,107 +57,52 @@ async def test_create_audio_calls_grpc_then_es() -> None: ) assert result["id"] == "abc123" - assert result["audio_name"] == "雨声" - assert result["audio_url"] == "https://cdn.example.com/a.mp3" - comm.create_audio_material.assert_awaited_once() - create_req = comm.create_audio_material.await_args.args[0] - assert create_req.audio_name == "雨声" - assert create_req.audio_url == "https://cdn.example.com/a.mp3" - comm.list_audio_materials_by_name.assert_awaited_once_with("雨声") + materials.insert_material.assert_awaited_once() es_sync.upsert_somni_material.assert_awaited_once() assert es_sync.upsert_somni_material.await_args.args[0] == "abc123" search_cache.clear_all.assert_awaited_once() @pytest.mark.asyncio -async def test_update_audio_calls_grpc_then_es() -> None: - comm = MagicMock() - comm.update_audio_material = AsyncMock() +async def test_update_audio_writes_mongo_then_es() -> None: + materials = MagicMock() + materials.update_material = AsyncMock( + return_value={"id": "m1", "description": "新描述"} + ) es_sync = MagicMock() es_sync.upsert_somni_material = AsyncMock() search_cache = MagicMock() search_cache.clear_all = AsyncMock() - service = _service(comm=comm, es_sync=es_sync, search_cache=search_cache) + service = _service(materials=materials, es_sync=es_sync, search_cache=search_cache) await service.update_audio( - "abc123", - UpdateAudioRequest.model_validate({"description": "新描述"}), + "m1", UpdateAudioRequest.model_validate({"description": "新描述"}) ) - - comm.update_audio_material.assert_awaited_once() - material_id_arg, update_body = comm.update_audio_material.await_args.args - assert material_id_arg == "abc123" - assert update_body.description == "新描述" + materials.update_material.assert_awaited_once() es_sync.upsert_somni_material.assert_awaited_once() - assert es_sync.upsert_somni_material.await_args.args[0] == "abc123" - search_cache.clear_all.assert_awaited_once() @pytest.mark.asyncio -async def test_delete_audio_clears_search_cache() -> None: - comm = MagicMock() - comm.delete_audio_material = AsyncMock() +async def test_delete_audio_deletes_mongo_and_es() -> None: + materials = MagicMock() + materials.delete_material = AsyncMock() es_sync = MagicMock() es_sync.delete_audio = AsyncMock() search_cache = MagicMock() search_cache.clear_all = AsyncMock() - service = _service(comm=comm, es_sync=es_sync, search_cache=search_cache) - - await service.delete_audio("abc123") - - comm.delete_audio_material.assert_awaited_once_with("abc123") - es_sync.delete_audio.assert_awaited_once_with("abc123") - search_cache.clear_all.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_search_audio_returns_cache_hit_without_retrieval() -> None: - retrieval = MagicMock() - retrieval.search = AsyncMock() - search_cache = MagicMock() - search_cache.get = AsyncMock(return_value=[{"_id": "cached"}]) - search_cache.set = AsyncMock() - service = _service(retrieval=retrieval, search_cache=search_cache) - request = SearchAudioRequest(query_text="雨声") - - result = await service.search_audio(request) - - assert result.materials == [{"_id": "cached"}] - search_cache.get.assert_awaited_once_with(request) - retrieval.search.assert_not_awaited() - search_cache.set.assert_not_awaited() - - -@pytest.mark.asyncio -async def test_search_audio_miss_runs_retrieval_and_sets_cache() -> None: - retrieval = MagicMock() - retrieval.search = AsyncMock(return_value=[{"id": "fresh"}]) - search_cache = MagicMock() - search_cache.get = AsyncMock(return_value=None) - search_cache.set = AsyncMock() - service = _service(retrieval=retrieval, search_cache=search_cache) - request = SearchAudioRequest(query_text="雨声") - - result = await service.search_audio(request) + service = _service(materials=materials, es_sync=es_sync, search_cache=search_cache) - assert result.materials == [{"id": "fresh"}] - retrieval.search.assert_awaited_once_with(request) - search_cache.set.assert_awaited_once_with(request, [{"id": "fresh"}]) + await service.delete_audio("m1") + materials.delete_material.assert_awaited_once_with("m1") + es_sync.delete_audio.assert_awaited_once_with("m1") @pytest.mark.asyncio -async def test_search_audio_empty_result_skips_cache_write() -> None: - """空结果不写缓存,避免长时间缓存无命中。""" +async def test_search_audio_uses_retrieval() -> None: retrieval = MagicMock() - retrieval.search = AsyncMock(return_value=[]) - search_cache = MagicMock() - search_cache.get = AsyncMock(return_value=None) - search_cache.set = AsyncMock() - service = _service(retrieval=retrieval, search_cache=search_cache) - request = SearchAudioRequest(query_text="不存在的声音") - - result = await service.search_audio(request) - - assert result.materials == [] - retrieval.search.assert_awaited_once_with(request) - search_cache.set.assert_not_awaited() + retrieval.search = AsyncMock(return_value=[{"id": "m1"}]) + retrieval.clear_sleep_stage_cache = AsyncMock() + retrieval.warm_sleep_stage_cache = AsyncMock() + service = _service(retrieval=retrieval) + data = await service.search_audio(SearchAudioRequest(query_text="雨")) + assert data.materials == [{"id": "m1"}] diff --git a/tests/test_comm_client.py b/tests/test_comm_client.py deleted file mode 100644 index 82b8568..0000000 --- a/tests/test_comm_client.py +++ /dev/null @@ -1,118 +0,0 @@ -"""CommClient Somni Create/Update 请求映射单测。""" - -from __future__ import annotations - -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from app.bionode_grpc_clients.comm import AUDIO_MATERIAL_STATUS_PUBLISHED, CommClient -from app.bionode_grpc_clients.comm.grpc_gen import bionode_comm_pb2, bionode_common_pb2 -from app.core.config import Settings -from app.schemas.audio import CreateAudioRequest, UpdateAudioRequest - - -@pytest.mark.asyncio -async def test_create_audio_material_maps_somni_fields() -> None: - client = CommClient(Settings()) - stub = MagicMock() - stub.CreateAudioMaterial = AsyncMock(return_value=bionode_comm_pb2.EmptyRes()) - client._stub = stub - - await client.create_audio_material( - CreateAudioRequest.model_validate( - { - "audio_name": "雨声", - "audio_url": "https://cdn.example.com/a.mp3", - "operation_type": 1, - "created_by": "agent", - "sleep_stage_tags": [ - {"tag_id": "t1", "code": "unwind", "name": "放松"} - ], - "audio_engineering_tags": [ - { - "tag_id": "e1", - "code": "event_density", - "name": "密度", - "value": {"tag_id": "v1", "code": "low", "name": "低"}, - "band_values": [0.1, 0.2], - "relative_loudness": -3.5, - } - ], - } - ) - ) - - req = stub.CreateAudioMaterial.await_args.args[0] - assert req.audio_name == "雨声" - assert req.audio_url == "https://cdn.example.com/a.mp3" - assert req.operation_type == 1 - assert req.created_by == "agent" - assert len(req.sleep_stage_tags) == 1 - assert req.sleep_stage_tags[0].code == "unwind" - assert len(req.audio_engineering_tags) == 1 - eng = req.audio_engineering_tags[0] - assert eng.value.code == "low" - assert list(eng.band_values) == [0.1, 0.2] - assert eng.relative_loudness == pytest.approx(-3.5) - - -@pytest.mark.asyncio -async def test_update_audio_material_maps_partial_and_status() -> None: - client = CommClient(Settings()) - stub = MagicMock() - stub.UpdateAudioMaterial = AsyncMock(return_value=bionode_comm_pb2.EmptyRes()) - client._stub = stub - - await client.update_audio_material( - "abc123", - UpdateAudioRequest.model_validate({"description": "新描述", "status": False}), - ) - - req = stub.UpdateAudioMaterial.await_args.args[0] - assert req.id == "abc123" - assert req.description == "新描述" - assert req.status is False - assert not req.HasField("audio_name") - - -@pytest.mark.asyncio -async def test_list_audio_materials_by_name_uses_published_status() -> None: - client = CommClient(Settings()) - stub = MagicMock() - stub.ListAudioMaterials = AsyncMock( - return_value=bionode_comm_pb2.AudioMaterialListRes(materials=[]) - ) - client._stub = stub - - await client.list_audio_materials_by_name("测试音频") - - call_args = stub.ListAudioMaterials.await_args[0][0] - assert call_args.name == "测试音频" - assert call_args.status is AUDIO_MATERIAL_STATUS_PUBLISHED - assert call_args.page.order_by == "create_time desc" - - -@pytest.mark.asyncio -async def test_list_audio_materials_page_uses_published_and_pagination() -> None: - client = CommClient(Settings()) - stub = MagicMock() - material = bionode_comm_pb2.AudioMaterialInfo(id="abc", audio_name="海浪声白噪音") - stub.ListAudioMaterials = AsyncMock( - return_value=bionode_comm_pb2.AudioMaterialListRes( - materials=[material], - page=bionode_common_pb2.PageResponse(total=1, page=1, page_size=50), - ) - ) - client._stub = stub - - materials, total = await client.list_audio_materials_page(page=2, page_size=50) - - assert len(materials) == 1 - assert materials[0].id == "abc" - assert total == 1 - call_args = stub.ListAudioMaterials.await_args[0][0] - assert call_args.status is AUDIO_MATERIAL_STATUS_PUBLISHED - assert call_args.page.page == 2 - assert call_args.page.page_size == 50 - assert call_args.page.order_by == "update_time desc" diff --git a/tests/test_comm_grpc.py b/tests/test_comm_grpc.py deleted file mode 100644 index b2fec6f..0000000 --- a/tests/test_comm_grpc.py +++ /dev/null @@ -1,30 +0,0 @@ -"""comm-service gRPC 集成探测(默认跳过,需显式开启)。 - -运行: - COMM_GRPC_INTEGRATION=1 pytest tests/test_comm_grpc.py -v -""" - -from __future__ import annotations - -import os - -import pytest - -from app.bionode_grpc_clients import CommClient -from app.core.config import get_settings - - -@pytest.mark.skipif( - os.getenv("COMM_GRPC_INTEGRATION") != "1", - reason="设置 COMM_GRPC_INTEGRATION=1 才连真实 comm-service", -) -@pytest.mark.asyncio -async def test_comm_grpc_ping() -> None: - settings = get_settings() - client = CommClient(settings) - await client.connect() - try: - tag_count = await client.ping(timeout_sec=15.0) - finally: - await client.close() - assert tag_count >= 0 diff --git a/tests/test_es_search.py b/tests/test_es_search.py index a39ed13..02063f8 100644 --- a/tests/test_es_search.py +++ b/tests/test_es_search.py @@ -8,6 +8,18 @@ from app.es.search import SEARCH_CANDIDATE_SOURCE_INCLUDES, EsSearch +def test_es_search_accepts_isolated_index_names() -> None: + search = EsSearch( + MagicMock(), + Settings(), + audio_index="somni-prod-audio", + tag_dictionary_index="somni-prod-tags", + ) + + assert search.audio_index == "somni-prod-audio" + assert search.tag_dictionary_index == "somni-prod-tags" + + @pytest.mark.asyncio async def test_get_dictionary_vectors_deduplicates_and_batches_ids() -> None: client = MagicMock() diff --git a/tests/test_grpc_audio.py b/tests/test_grpc_audio.py new file mode 100644 index 0000000..b387c01 --- /dev/null +++ b/tests/test_grpc_audio.py @@ -0,0 +1,75 @@ +"""功能手板 AudioRpc。""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock + +import grpc +import pytest + +from app.schemas.audio import SearchAudioData +from app.server.handboard.audio.rpc import AudioRpc +from app.uburnode_grpc.grpc_gen import uburnode_pb2 + + +def _context() -> MagicMock: + ctx = MagicMock() + ctx.abort = AsyncMock(side_effect=grpc.aio.AbortError) + return ctx + + +@pytest.mark.asyncio +async def test_create_audio_maps_response() -> None: + service = MagicMock() + service.create_audio = AsyncMock( + return_value={ + "id": "m1", + "audio_name": "夜雨", + "description": "", + "status": True, + "create_time": "", + "update_time": "", + "audio_url": "", + "operation_type": 0, + "created_by": "", + "updated_by": "", + "sleep_stage_tags": [], + "content_form_tags": [], + "mechanism_tags": [], + "audio_engineering_tags": [], + "medical_risk_tags": [], + "evidence_level_tags": [], + "embedding": [0.1, 0.2], + } + ) + rpc = AudioRpc(service) + res = await rpc.CreateAudio(uburnode_pb2.CreateAudioReq(audio_name="夜雨"), _context()) + assert res.material.id == "m1" + assert list(res.material.embedding) == [0.1, 0.2] + + +@pytest.mark.asyncio +async def test_update_delete_ok() -> None: + service = MagicMock() + service.update_audio = AsyncMock() + service.delete_audio = AsyncMock() + rpc = AudioRpc(service) + ctx = _context() + upd = await rpc.UpdateAudio( + uburnode_pb2.UpdateAudioReq(material_id="m1", description="x"), + ctx, + ) + assert upd.ok is True + deleted = await rpc.DeleteAudio(uburnode_pb2.IdRequest(id="m1"), ctx) + assert deleted.ok is True + + +@pytest.mark.asyncio +async def test_search_returns_structs() -> None: + service = MagicMock() + service.search_audio = AsyncMock( + return_value=SearchAudioData(materials=[{"id": "m1", "audio_name": "a"}]) + ) + rpc = AudioRpc(service) + res = await rpc.SearchAudio(uburnode_pb2.SearchAudioReq(), _context()) + assert res.materials[0].fields["id"].string_value == "m1" diff --git a/tests/test_grpc_quiz.py b/tests/test_grpc_quiz.py new file mode 100644 index 0000000..cf4e69c --- /dev/null +++ b/tests/test_grpc_quiz.py @@ -0,0 +1,218 @@ +"""功能手板 / 量产 QuizRpc。""" + +from __future__ import annotations + +import json +from unittest.mock import AsyncMock, MagicMock + +import grpc +import pytest +from bson import ObjectId +from google.protobuf.json_format import MessageToDict + +from app.core.config import Settings +from app.core.exceptions import AppError, MongoNotConfiguredError +from app.server.handboard.quiz.rpc import QuizRpc as HandboardQuizRpc +from app.server.somni.quiz.rpc import QuizRpc as SomniQuizRpc +from app.server.somni.quiz.service import QuizService +from app.uburnode_grpc.grpc_gen import uburnode_pb2, uburnode_somni_pb2 + + +def _context() -> MagicMock: + ctx = MagicMock() + ctx.abort = AsyncMock(side_effect=grpc.aio.AbortError) + return ctx + + +@pytest.mark.asyncio +async def test_handboard_get_answer_empty() -> None: + rpc = HandboardQuizRpc() + res = await rpc.GetAnswer( + uburnode_pb2.GetAnswerReq(uid="u1", answer_id="a1"), + _context(), + ) + assert res == uburnode_pb2.GetAnswerRes() + + +@pytest.mark.asyncio +async def test_handboard_get_answer_requires_fields() -> None: + rpc = HandboardQuizRpc() + with pytest.raises(grpc.aio.AbortError): + await rpc.GetAnswer(uburnode_pb2.GetAnswerReq(uid="", answer_id="a1"), _context()) + + +@pytest.mark.asyncio +async def test_somni_get_answer_requires_fields() -> None: + rpc = SomniQuizRpc(MagicMock()) + with pytest.raises(grpc.aio.AbortError): + await rpc.GetAnswer(uburnode_somni_pb2.GetAnswerReq(), _context()) + + +@pytest.mark.asyncio +async def test_somni_get_answer_maps_answer_item() -> None: + service = MagicMock() + service.get_answer = AsyncMock( + return_value={ + "answers": [ + { + "question_id": "q1", + "input_type": "radio", + "title": "您的性别是?", + "tags": [], + "value": {"option_id": "A", "option_text": "先生"}, + "extra_input": "", + }, + { + "question_id": "q3", + "input_type": "checkbox", + "title": "关注阶段", + "value": [ + {"option_id": "A", "option_text": "早期"}, + {"option_id": "B", "option_text": "VC"}, + ], + "extra_input": "", + }, + { + "question_id": "q2", + "input_type": "input_number", + "title": "夜醒次数", + "value": 2, + "extra_input": "", + }, + { + "question_id": "q4", + "input_type": "input", + "title": "补充说明", + "value": "最近压力比较大", + "extra_input": "", + }, + { + "question_id": "q5", + "input_type": "switch", + "title": "是否启用", + "value": True, + "extra_input": "", + }, + ] + } + ) + rpc = SomniQuizRpc(service) + res = await rpc.GetAnswer( + uburnode_somni_pb2.GetAnswerReq(uid="u1", answer_id="a1"), + _context(), + ) + payload = MessageToDict(res, preserving_proto_field_name=True) + answers = json.loads(payload["answers"]) + assert len(answers) == 5 + assert answers[0] == { + "question_id": "q1", + "input_type": "radio", + "title": "您的性别是?", + "value": {"option_id": "A", "option_text": "先生"}, + "extra_input": "", + } + assert "tags" not in answers[0] + assert answers[1]["value"] == [ + {"option_id": "A", "option_text": "早期"}, + {"option_id": "B", "option_text": "VC"}, + ] + assert answers[2]["value"] == 2 + assert answers[3]["value"] == "最近压力比较大" + assert answers[4]["value"] is True + assert json.loads(res.answers)[0]["title"] == "您的性别是?" + +@pytest.mark.asyncio +async def test_somni_quiz_service_loads_from_collection() -> None: + doc = { + "_id": "69b10cc516d7472aedf6bb80", + "uid": "user-001", + "answers": [ + { + "question_id": "q1", + "input_type": "radio", + "title": "您的性别是?", + "value": {"option_id": "A", "option_text": "先生"}, + }, + { + "question_id": "q3", + "input_type": "select", + "title": "常驻城市", + "value": {"option_id": "A", "option_text": "北京"}, + }, + { + "question_id": "q4", + "input_type": "checkbox", + "title": "关注阶段", + "value": [ + {"option_id": "A", "option_text": "早期"}, + {"option_id": "B", "option_text": "VC"}, + ], + }, + { + "question_id": "q2", + "input_type": "input", + "title": "补充说明", + "value": "最近压力比较大", + "extra_input": "备注", + }, + ], + } + collection = MagicMock() + collection.find_one = AsyncMock(return_value=doc) + db = MagicMock() + db.__getitem__ = MagicMock(return_value=collection) + client = MagicMock() + client.__getitem__ = MagicMock(return_value=db) + + settings = Settings( + somni_mongo_db="Somni", + somni_mongo_answers_collection="somni_quiz_answers", + ) + svc = QuizService(client, settings) + payload = await svc.get_answer("user-001", "69b10cc516d7472aedf6bb80") + + assert settings.somni_mongo_answers_collection == "somni_quiz_answers" + client.__getitem__.assert_called_with("Somni") + db.__getitem__.assert_called_with("somni_quiz_answers") + assert payload["answers"][0]["input_type"] == "radio" + assert payload["answers"][0]["value"] == { + "option_id": "A", + "option_text": "先生", + } + assert payload["answers"][1]["value"] == { + "option_id": "A", + "option_text": "北京", + } + assert payload["answers"][2]["value"] == [ + {"option_id": "A", "option_text": "早期"}, + {"option_id": "B", "option_text": "VC"}, + ] + assert payload["answers"][3]["extra_input"] == "备注" + assert "tags" not in payload["answers"][0] + query = collection.find_one.await_args.args[0] + assert query == { + "uid": "user-001", + "_id": ObjectId("69b10cc516d7472aedf6bb80"), + } + + +@pytest.mark.asyncio +async def test_somni_quiz_service_not_found() -> None: + collection = MagicMock() + collection.find_one = AsyncMock(return_value=None) + db = MagicMock() + db.__getitem__ = MagicMock(return_value=collection) + client = MagicMock() + client.__getitem__ = MagicMock(return_value=db) + + svc = QuizService(client, Settings(somni_mongo_answers_collection="somni_quiz_answers")) + with pytest.raises(AppError) as exc: + await svc.get_answer("u1", "missing-id") + assert exc.value.status_code == 404 + + +@pytest.mark.asyncio +async def test_somni_quiz_service_requires_mongo() -> None: + svc = QuizService(None, Settings()) + with pytest.raises(MongoNotConfiguredError): + await svc.get_answer("u1", "a1") diff --git a/tests/test_grpc_report.py b/tests/test_grpc_report.py new file mode 100644 index 0000000..8c6fd79 --- /dev/null +++ b/tests/test_grpc_report.py @@ -0,0 +1,120 @@ +"""量产 ReportRpc。""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock + +import grpc +import pytest +from google.protobuf.json_format import MessageToDict + +from app.server.somni.report.rpc import ReportRpc +from app.uburnode_grpc.grpc_gen import uburnode_somni_pb2 + + +def _context() -> MagicMock: + ctx = MagicMock() + ctx.abort = AsyncMock(side_effect=grpc.aio.AbortError) + return ctx + + +def _make_rpc() -> tuple[ReportRpc, MagicMock]: + service = MagicMock() + service.get_summary = AsyncMock( + return_value={ + "sleep_summary": { + "body_battery": 90, + "body_battery_status": "Energy At Its Peak", + "total_minutes": 457, + "deep_sleep_minutes": 95, + "avg_heart_rate": 62, + "avg_respiratory_rate": 15, + } + } + ) + service.get_events = AsyncMock( + return_value={ + "record_date": "2026-08-24", + "sleep_events": [], + "event_count": 0, + "abnormal_count": 0, + "intervention_count": 0, + "idf_data": [], + "physio_data": [], + "env_data": [], + } + ) + service.get_environment = AsyncMock( + return_value={ + "environment_summary": { + "temperature": {"value": 23, "min": 21, "max": 25}, + "humidity": {"value": 52, "min": 40, "max": 60}, + "illuminance": {"value": 1, "min": 0, "max": 5}, + "noise": {"value": 28, "min": 20, "max": 35}, + } + } + ) + service.get_structure = AsyncMock( + return_value={ + "sleep_structure": { + "awake": {"minutes": 20, "percent": 4}, + "rem_sleep": {"minutes": 100, "percent": 20}, + "light_sleep": {"minutes": 250, "percent": 50}, + "deep_sleep": {"minutes": 90, "percent": 18}, + } + } + ) + service.get_sleep_quality = AsyncMock( + return_value={ + "sleep_quality": { + "time_in_bed_minutes": 500, + "sleep_onset_latency_minutes": 20, + "sleep_efficiency": 92, + "bedtime": "23:30", + "wake_up_time": "07:30", + "awake_after_onset_minutes": 18, + } + } + ) + return ReportRpc(service), service + + +@pytest.mark.asyncio +async def test_report_requires_uid_and_record_date() -> None: + rpc, _service = _make_rpc() + with pytest.raises(grpc.aio.AbortError): + await rpc.GetSummary( + uburnode_somni_pb2.ReportDateReq(uid="", record_date="2026-08-24"), + _context(), + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("method_name", "service_name"), + [ + ("GetSummary", "get_summary"), + ("GetEvents", "get_events"), + ("GetEnvironment", "get_environment"), + ("GetStructure", "get_structure"), + ("GetSleepQuality", "get_sleep_quality"), + ], +) +async def test_report_rpcs_call_service(method_name: str, service_name: str) -> None: + rpc, service = _make_rpc() + req = uburnode_somni_pb2.ReportDateReq(uid="u1", record_date="2026-08-24") + res = await getattr(rpc, method_name)(req, _context()) + getattr(service, service_name).assert_awaited_once_with("u1", "2026-08-24") + assert res is not None + + +@pytest.mark.asyncio +async def test_get_summary_maps_fields() -> None: + rpc, _service = _make_rpc() + res = await rpc.GetSummary( + uburnode_somni_pb2.ReportDateReq(uid="u1", record_date="2026-08-24"), + _context(), + ) + payload = MessageToDict(res, preserving_proto_field_name=True) + assert payload["sleep_summary"]["avg_heart_rate"] == 62 + assert payload["sleep_summary"]["body_battery_status"] == "Energy At Its Peak" diff --git a/tests/test_grpc_server.py b/tests/test_grpc_server.py new file mode 100644 index 0000000..e67ea48 --- /dev/null +++ b/tests/test_grpc_server.py @@ -0,0 +1,47 @@ +"""手板 / 量产 gRPC bootstrap。""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from app.core.config import Settings +from app.main import AppState +from app.server.bootstrap import start_grpc_servers, stop_grpc_servers + + +@pytest.mark.asyncio +async def test_start_both_servers_when_enabled(monkeypatch: pytest.MonkeyPatch) -> None: + fake_hb = MagicMock() + fake_hb.add_insecure_port = MagicMock(return_value=50065) + fake_hb.start = AsyncMock() + fake_sm = MagicMock() + fake_sm.add_insecure_port = MagicMock(return_value=50064) + fake_sm.start = AsyncMock() + servers = iter([fake_hb, fake_sm]) + monkeypatch.setattr( + "app.server.bootstrap.grpc.aio.server", + lambda: next(servers), + ) + settings = Settings(grpc_enabled=True, somni_grpc_enabled=True) + state = AppState(settings=settings) + result = await start_grpc_servers(state, settings) + assert result.handboard is fake_hb + assert result.somni is fake_sm + fake_hb.start.assert_awaited_once() + fake_sm.start.assert_awaited_once() + fake_hb.stop = AsyncMock() + fake_sm.stop = AsyncMock() + await stop_grpc_servers(result) + fake_hb.stop.assert_awaited_once() + fake_sm.stop.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_disabled_skips_servers() -> None: + settings = Settings(grpc_enabled=False, somni_grpc_enabled=False) + state = AppState(settings=settings) + result = await start_grpc_servers(state, settings) + assert result.handboard is None + assert result.somni is None diff --git a/tests/test_grpc_somni_audio.py b/tests/test_grpc_somni_audio.py new file mode 100644 index 0000000..c58a60a --- /dev/null +++ b/tests/test_grpc_somni_audio.py @@ -0,0 +1,141 @@ +"""量产 AudioRpc。""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from app.server.somni.audio.rpc import AudioRpc +from app.uburnode_grpc.grpc_gen import uburnode_somni_pb2 + + +def _context() -> MagicMock: + return MagicMock() + + +def _make_rpc() -> tuple[AudioRpc, MagicMock]: + service = MagicMock() + service.get_audio = AsyncMock() + service.get_audio_tag = AsyncMock() + service.get_hot = AsyncMock() + return AudioRpc(service), service + + +@pytest.mark.asyncio +async def test_get_audio_passes_page_and_query() -> None: + rpc, service = _make_rpc() + service.get_audio = AsyncMock( + return_value={ + "list": [ + { + "id": "m1", + "audio_name": "雨声", + "audio_url": "https://cdn.example/a.mp3", + "cover_url": "https://cdn.example/a.png", + "description": "desc", + "vip": 0, + } + ], + "page": 1, + "page_size": 20, + "total": 1, + } + ) + req = uburnode_somni_pb2.GetAudioReq(page=1, page_size=20, query_text="雨声") + res = await rpc.GetAudio(req, _context()) + service.get_audio.assert_awaited_once_with( + page=1, + page_size=20, + fetch_all=False, + query_text="雨声", + tag_code="", + ) + assert res.list[0].audio_name == "雨声" + assert res.list[0].vip == 0 + assert res.total == 1 + assert res.page == 1 + assert res.page_size == 20 + + +@pytest.mark.asyncio +async def test_get_audio_fetch_all() -> None: + rpc, service = _make_rpc() + service.get_audio = AsyncMock( + return_value={ + "list": [], + "page": 1, + "page_size": 0, + "total": 0, + } + ) + req = uburnode_somni_pb2.GetAudioReq(fetch_all=True) + await rpc.GetAudio(req, _context()) + service.get_audio.assert_awaited_once_with( + page=None, + page_size=None, + fetch_all=True, + query_text="", + tag_code="", + ) + + +@pytest.mark.asyncio +async def test_get_audio_passes_tag_code() -> None: + rpc, service = _make_rpc() + service.get_audio = AsyncMock( + return_value={ + "list": [], + "page": 1, + "page_size": 20, + "total": 0, + } + ) + req = uburnode_somni_pb2.GetAudioReq(tag_code="steady_rain") + await rpc.GetAudio(req, _context()) + service.get_audio.assert_awaited_once_with( + page=None, + page_size=None, + fetch_all=False, + query_text="", + tag_code="steady_rain", + ) + + +@pytest.mark.asyncio +async def test_get_audio_tag_maps_fields() -> None: + rpc, service = _make_rpc() + service.get_audio_tag = AsyncMock( + return_value={ + "tags": [ + { + "type": "content_form", + "code": "natural_sound", + "name": "自然声", + "name_en": "Natural Sound", + "id": "root-natural", + "parent_tag_id": None, + "status": "启用", + } + ] + } + ) + res = await rpc.GetAudioTag(uburnode_somni_pb2.GetAudioTagReq(), _context()) + service.get_audio_tag.assert_awaited_once_with() + assert res.tags[0].code == "natural_sound" + assert res.tags[0].name_en == "Natural Sound" + assert res.tags[0].id == "root-natural" + assert res.tags[0].parent_tag_id.WhichOneof("kind") == "null_value" + assert res.tags[0].status == "启用" + + +@pytest.mark.asyncio +async def test_get_hot_maps_items() -> None: + rpc, service = _make_rpc() + service.get_hot = AsyncMock( + return_value={"items": [{"keyword": "雨声", "score": 5}]} + ) + res = await rpc.GetHot(uburnode_somni_pb2.GetHotReq(), _context()) + service.get_hot.assert_awaited_once_with() + assert res.items[0].keyword == "雨声" + assert res.items[0].score == 5 diff --git a/tests/test_logging.py b/tests/test_logging.py index 28332ee..768c4bf 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -4,7 +4,12 @@ from loguru import logger from app.core.config import Settings -from app.core.logging import LOG_FILE_SUFFIX, current_log_path, setup_logging +from app.core.logging import ( + LOG_FILE_SUFFIX, + current_log_path, + parse_log_size, + setup_logging, +) def test_current_log_path_uses_date_and_suffix(tmp_path: Path) -> None: @@ -14,6 +19,12 @@ def test_current_log_path_uses_date_and_suffix(tmp_path: Path) -> None: assert path.name == "2026-07-15_ubur_log" +def test_parse_log_size_matches_common_units() -> None: + assert parse_log_size("100 MB") == 100_000_000 + assert parse_log_size("1 KiB") == 1024 + assert parse_log_size("512 B") == 512 + + def test_setup_logging_creates_today_dated_file(tmp_path: Path) -> None: settings = Settings(log_dir=str(tmp_path), log_level="INFO") log_file = setup_logging(settings) @@ -43,3 +54,28 @@ def test_file_log_uses_default_request_id_when_absent(tmp_path: Path) -> None: assert "无上下文的探测日志" in content # 缺省占位,避免 extra[request_id] KeyError assert " | - | " in content or content.count("| - |") >= 1 + + +def test_size_rotation_archives_full_file_and_continues_same_day( + tmp_path: Path, +) -> None: + settings = Settings( + log_dir=str(tmp_path), + log_level="INFO", + log_rotation_size="300 B", + ) + active = setup_logging(settings) + for index in range(40): + logger.info("size-rotation-probe-{}", index) + logger.complete() + + day_prefix = f"{date.today():%Y-%m-%d}{LOG_FILE_SUFFIX}" + archived = [ + path + for path in tmp_path.iterdir() + if path.name.startswith(day_prefix) and path.name != day_prefix + ] + assert archived, "超大小后应归档至少一个带时间戳后缀的当日文件" + assert active.exists() + assert active.name == day_prefix + assert "size-rotation-probe" in active.read_text(encoding="utf-8") diff --git a/tests/test_materials_store.py b/tests/test_materials_store.py index a8b82bd..fcfe63e 100644 --- a/tests/test_materials_store.py +++ b/tests/test_materials_store.py @@ -9,7 +9,7 @@ from bson import ObjectId from app.core.config import Settings -from app.mongo.materials import MaterialsStore +from app.server.handboard.audio.store import MaterialsStore def _store_with_collection() -> tuple[MaterialsStore, MagicMock]: diff --git a/tests/test_retrieval.py b/tests/test_retrieval.py index bed3c76..f152e9e 100644 --- a/tests/test_retrieval.py +++ b/tests/test_retrieval.py @@ -16,7 +16,10 @@ VOICE_MARKER, RetrievalService, ScoredCandidate, + VectorSimilaritySnapshot, _apply_voice_code_filter, + _cosine_similarity, + _cosine_similarity_matrix, _match_labels_from_text, _normalize_to_dictionary_labels, _strip_voice_mention_tags, @@ -28,6 +31,42 @@ _VECTOR_DIM = 512 +def test_vectorized_cosine_matches_scalar_reference() -> None: + requests = [[1.0, 2.0, 3.0], [0.0, 0.0, 0.0], [-1.0, 0.5, 2.0]] + dictionary = [[3.0, 2.0, 1.0], [0.0, 0.0, 0.0], [1.0, -2.0, 0.5]] + + actual = _cosine_similarity_matrix(requests, dictionary) + + for row, request in enumerate(requests): + for column, candidate in enumerate(dictionary): + assert actual[row, column] == pytest.approx( + _cosine_similarity(request, candidate), + abs=1e-12, + ) + + +def test_vectorized_cosine_rejects_dimension_mismatch() -> None: + with pytest.raises(ValueError, match="dimension mismatch"): + _cosine_similarity_matrix([[1.0, 0.0]], [[1.0, 0.0, 0.0]]) + + +def test_similarity_snapshot_preserves_exclusive_tag_semantics() -> None: + snapshot = VectorSimilaritySnapshot.build( + [[1.0, 0.0], [1.0, 0.0]], + {"rain": [1.0, 0.0]}, + ) + + assert ( + snapshot.count_matches( + ["rain"], + ["白噪音", "雨声"], + row_offset=0, + threshold=0.8, + ) + == 1 + ) + + def _tag_entries(prefix: str, labels: list[str]) -> list[dict[str, str]]: return [{"tag_id": f"{prefix}_{label}", "code": label, "name": label} for label in labels] @@ -171,6 +210,47 @@ async def test_search_uses_sleep_stage_cache_when_available() -> None: es_search.filter_by_sleep_stage.assert_not_called() +@pytest.mark.asyncio +async def test_search_cache_miss_uses_on_demand_stage_loader() -> None: + cached_doc = _audio_doc("唤醒音乐", sleep_stage=["唤醒"], content_form=["音乐"]) + sleep_cache = MagicMock() + sleep_cache.get = AsyncMock(return_value=None) + sleep_cache.get_or_load = AsyncMock(return_value=[cached_doc]) + service, es_search, _encoder = _build_service() + service._sleep_stage_cache = sleep_cache + es_search.parse_tags = EsSearch.parse_tags + + results = await service.search( + SearchAudioRequest(sleep_stage_tags=["唤醒"], content_tags=["音乐"], top_k=5) + ) + + assert [result["audio_name"] for result in results] == ["唤醒音乐"] + sleep_cache.get_or_load.assert_awaited_once_with( + ["唤醒"], + service._load_sleep_stage_candidates, + ) + es_search.filter_by_sleep_stage.assert_not_called() + + +@pytest.mark.asyncio +async def test_search_cache_loader_error_falls_back_to_es() -> None: + es_doc = _audio_doc("ES 唤醒音乐", sleep_stage=["唤醒"], content_form=["音乐"]) + sleep_cache = MagicMock() + sleep_cache.get = AsyncMock(return_value=None) + sleep_cache.get_or_load = AsyncMock(side_effect=RuntimeError("Redis unavailable")) + service, es_search, _encoder = _build_service() + service._sleep_stage_cache = sleep_cache + es_search.parse_tags = EsSearch.parse_tags + es_search.filter_by_sleep_stage = AsyncMock(return_value=[es_doc]) + + results = await service.search( + SearchAudioRequest(sleep_stage_tags=["唤醒"], content_tags=["音乐"], top_k=5) + ) + + assert [result["audio_name"] for result in results] == ["ES 唤醒音乐"] + es_search.filter_by_sleep_stage.assert_awaited_once_with(["唤醒"]) + + @pytest.mark.asyncio async def test_search_merges_multi_stage_cache_via_service_path() -> None: """多睡眠阶段请求走缓存 get,由缓存侧按 audio_url 去重。""" @@ -367,9 +447,7 @@ async def test_text_query_normalizes_color_noise_alias_and_excludes_siblings( canonical_doc = _audio_doc( canonical, content_form=["颜色噪音", canonical], description_score=1.0 ) - sibling_doc = _audio_doc( - sibling, content_form=["颜色噪音", sibling], description_score=1.0 - ) + sibling_doc = _audio_doc(sibling, content_form=["颜色噪音", sibling], description_score=1.0) service, es_search, encoder = _build_service( settings=Settings(search_sleep_stage_filter_enabled=False) ) @@ -468,9 +546,7 @@ async def test_search_keeps_candidate_when_disliked_vector_below_threshold() -> ) es_search.parse_tags = EsSearch.parse_tags encoder.encode = AsyncMock( - side_effect=lambda texts: [ - unit_vec if t == "白噪音" else orthogonal_vec for t in texts - ], + side_effect=lambda texts: [unit_vec if t == "白噪音" else orthogonal_vec for t in texts], ) es_search.get_dictionary_vectors = AsyncMock(return_value={"cf_白噪音": unit_vec}) request = SearchAudioRequest( @@ -607,7 +683,6 @@ def track_coarse(candidates, *args, **kwargs): assert [r["audio_name"] for r in results] == ["保留少命中"] - @pytest.mark.asyncio async def test_search_returns_all_when_top_k_omitted() -> None: """未传 top_k 时返回全部候选,不截断。""" @@ -667,6 +742,7 @@ async def test_search_returns_projected_material() -> None: "audio_name": "雨声A", "description": "描述", "audio_url": doc["audio_url"], + "cover_url": "", "content_form_tags": [{"name": "雨声", "parent_tag_id": "p1"}], "audio_engineering_tags": [], } @@ -706,6 +782,7 @@ async def test_text_query_can_return_description_only_recall() -> None: "audio_name": "描述命中", "description": "海边声音", "audio_url": "https://cdn.example.com/描述命中.mp3", + "cover_url": "", "content_form_tags": [{"name": "海浪", "parent_tag_id": "p2"}], "audio_engineering_tags": [], } @@ -796,9 +873,7 @@ async def test_text_query_extracts_positive_and_negative_tags() -> None: ) encoder.encode_one = AsyncMock(return_value=unit_rain) encoder.encode = AsyncMock( - side_effect=lambda texts: [ - unit_noise if t == "嘈杂" else unit_rain for t in texts - ] + side_effect=lambda texts: [unit_noise if t == "嘈杂" else unit_rain for t in texts] ) es_search.get_dictionary_vectors = AsyncMock( return_value={"cf_雨声": unit_rain, "cf_嘈杂": unit_noise} @@ -1042,7 +1117,9 @@ async def test_warm_query_tag_vectors_encodes_dictionary_labels() -> None: ] ) encoder.encode = AsyncMock( - side_effect=lambda texts: [[float(i)] + [0.0] * (_VECTOR_DIM - 1) for i, _ in enumerate(texts)], + side_effect=lambda texts: [ + [float(i)] + [0.0] * (_VECTOR_DIM - 1) for i, _ in enumerate(texts) + ], ) await service.warm_query_tag_vectors() @@ -1074,9 +1151,7 @@ def test_dislike_penalty_respects_strong_threshold_from_settings() -> None: tags = EsSearch.parse_tags(_audio_doc("候选", content_form=["人声出现位置"])) dictionary = {"cf_人声出现位置": tag_vec} - soft_service, _, _ = _build_service( - settings=Settings(strong_dislike_sim_threshold=0.85) - ) + soft_service, _, _ = _build_service(settings=Settings(strong_dislike_sim_threshold=0.85)) soft_penalty = soft_service._dislike_penalty( tags, disliked_tags=["人声"], @@ -1085,9 +1160,7 @@ def test_dislike_penalty_respects_strong_threshold_from_settings() -> None: ) assert soft_penalty == 0.2 - hard_service, _, _ = _build_service( - settings=Settings(strong_dislike_sim_threshold=0.78) - ) + hard_service, _, _ = _build_service(settings=Settings(strong_dislike_sim_threshold=0.78)) hard_penalty = hard_service._dislike_penalty( tags, disliked_tags=["人声"], @@ -1104,15 +1177,17 @@ def test_tags_mention_voice_detects_substring() -> None: def test_strip_voice_mention_tags_removes_voice_related() -> None: - assert _strip_voice_mention_tags( - ["人声", "避免突发", "避免人声和语言引导", "节奏"] - ) == ["避免突发", "节奏"] + assert _strip_voice_mention_tags(["人声", "避免突发", "避免人声和语言引导", "节奏"]) == [ + "避免突发", + "节奏", + ] def test_usable_dislike_tags_ignores_event_density() -> None: - assert _usable_dislike_tags( - ["人声", "声音事件密度", "避免突发", "声音事件密度"] - ) == ["人声", "避免突发"] + assert _usable_dislike_tags(["人声", "声音事件密度", "避免突发", "声音事件密度"]) == [ + "人声", + "避免突发", + ] def test_voice_value_code_reads_nested_value() -> None: diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 1ad8c0f..cd8c02c 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -132,11 +132,12 @@ def test_cosine_similarity_identical() -> None: assert _cosine_similarity(vec, vec) == pytest.approx(1.0) -def test_audio_material_data_from_comm_material() -> None: - from app.bionode_grpc_clients.comm.grpc_gen import bionode_comm_pb2 +def test_audio_material_data_from_material_like() -> None: + from types import SimpleNamespace + from app.schemas.audio import AudioMaterialData - material = bionode_comm_pb2.AudioMaterialInfo( + material = SimpleNamespace( id="674a1b2c3d4e5f6789012345", description="夜雨", status=True, @@ -146,13 +147,17 @@ def test_audio_material_data_from_comm_material() -> None: audio_url="https://cdn.example.com/a.mp3", operation_type=0, created_by="agent", + updated_by="", sleep_stage_tags=[ - bionode_comm_pb2.AudioMaterialTag( - tag_id="t1", code="unwind", name="放松" - ) + SimpleNamespace(tag_id="t1", code="unwind", name="放松") ], + content_form_tags=[], + mechanism_tags=[], + audio_engineering_tags=[], + medical_risk_tags=[], + evidence_level_tags=[], ) - data = AudioMaterialData.from_comm_material(material) + data = AudioMaterialData.from_material_like(material) assert data.id == material.id assert data.audio_name == "深夜雨声" assert data.audio_url == "https://cdn.example.com/a.mp3" diff --git a/tests/test_search_material_projection.py b/tests/test_search_material_projection.py index 3bbbd80..75fbbfb 100644 --- a/tests/test_search_material_projection.py +++ b/tests/test_search_material_projection.py @@ -8,6 +8,7 @@ def test_project_keeps_only_api_fields() -> None: "audio_name": "雨声", "description": "轻柔雨声", "audio_url": "https://cdn/a.mp3", + "cover_url": "https://cdn/a-cover.jpg", "status": True, "sleep_stage_tags": [{"tag_id": "s1", "name": "放松"}], "mechanism_tags": [{"tag_id": "m1", "name": "放松"}], @@ -39,6 +40,7 @@ def test_project_keeps_only_api_fields() -> None: "audio_name": "雨声", "description": "轻柔雨声", "audio_url": "https://cdn/a.mp3", + "cover_url": "https://cdn/a-cover.jpg", "content_form_tags": [{"name": "雨声", "parent_tag_id": "p1"}], "audio_engineering_tags": [ {"code": "event_density", "value": {"code": "low"}} @@ -60,3 +62,11 @@ def test_project_missing_value_is_null() -> None: } ) assert out["audio_engineering_tags"] == [{"code": "tempo", "value": None}] + assert out["cover_url"] == "" + + +def test_project_cover_url_empty_when_absent() -> None: + out = project_search_material( + {"_id": "1", "audio_name": "a", "audio_url": "u"} + ) + assert out["cover_url"] == "" diff --git a/tests/test_sleep_stage_cache.py b/tests/test_sleep_stage_cache.py index 01ff44f..79218a3 100644 --- a/tests/test_sleep_stage_cache.py +++ b/tests/test_sleep_stage_cache.py @@ -2,8 +2,9 @@ from __future__ import annotations -import json +import asyncio import hashlib +import json from typing import Any import pytest @@ -64,13 +65,15 @@ def test_key_builders() -> None: assert SLEEP_STAGE_DOC_KEY_PREFIX == "sleep_stage_v2_doc:" assert build_sleep_stage_index_key("放松") == f"{SLEEP_STAGE_INDEX_KEY_PREFIX}放松" assert build_sleep_stage_doc_key("https://cdn/a.mp3").startswith(SLEEP_STAGE_DOC_KEY_PREFIX) - assert SLEEP_STAGES == ("放松", "入睡", "守护", "清醒") + assert SLEEP_STAGES == ("放松", "入睡", "守护", "唤醒") def test_merge_urls_preserve_order() -> None: - assert merge_urls_preserve_order( - [["https://a", "https://b"], ["https://a", "https://c"]] - ) == ["https://a", "https://b", "https://c"] + assert merge_urls_preserve_order([["https://a", "https://b"], ["https://a", "https://c"]]) == [ + "https://a", + "https://b", + "https://c", + ] @pytest.mark.asyncio @@ -163,6 +166,110 @@ async def loader(stage: str) -> list[dict[str, Any]]: assert json.loads(raw) == [f"https://cdn/{stage}.mp3"] +@pytest.mark.asyncio +async def test_get_or_load_only_populates_missing_stage() -> None: + redis = _FakeRedis() + cache = SleepStageCandidateCache(redis, ttl_sec=60) + relax = _doc("放松", "https://cdn/relax.mp3", ["放松"]) + wake = _doc("唤醒", "https://cdn/wake.mp3", ["唤醒"]) + await cache.set_stage("放松", [relax]) + calls: list[str] = [] + + async def loader(stage: str) -> list[dict[str, Any]]: + calls.append(stage) + return [wake] + + got = await cache.get_or_load(["放松", "唤醒"], loader) + + assert calls == ["唤醒"] + assert [doc["audio_url"] for doc in got] == [ + "https://cdn/relax.mp3", + "https://cdn/wake.mp3", + ] + assert await cache.get(["放松"]) == [relax] + + +@pytest.mark.asyncio +async def test_get_or_load_coalesces_concurrent_stage_misses() -> None: + redis = _FakeRedis() + cache = SleepStageCandidateCache(redis, ttl_sec=60) + wake = _doc("唤醒", "https://cdn/wake.mp3", ["唤醒"]) + started = asyncio.Event() + release = asyncio.Event() + calls = 0 + + async def loader(stage: str) -> list[dict[str, Any]]: + nonlocal calls + assert stage == "唤醒" + calls += 1 + started.set() + await release.wait() + return [wake] + + first = asyncio.create_task(cache.get_or_load(["唤醒"], loader)) + await started.wait() + second = asyncio.create_task(cache.get_or_load(["唤醒"], loader)) + release.set() + + assert await asyncio.gather(first, second) == [[wake], [wake]] + assert calls == 1 + + +@pytest.mark.asyncio +async def test_get_or_load_caches_empty_stage() -> None: + redis = _FakeRedis() + cache = SleepStageCandidateCache(redis, ttl_sec=60) + calls = 0 + + async def loader(stage: str) -> list[dict[str, Any]]: + nonlocal calls + calls += 1 + return [] + + assert await cache.get_or_load(["唤醒"], loader) == [] + assert await cache.get_or_load(["唤醒"], loader) == [] + assert calls == 1 + + +@pytest.mark.asyncio +async def test_get_or_load_does_not_cache_loader_error() -> None: + redis = _FakeRedis() + cache = SleepStageCandidateCache(redis, ttl_sec=60) + + async def failing_loader(stage: str) -> list[dict[str, Any]]: + raise RuntimeError("ES unavailable") + + with pytest.raises(RuntimeError, match="ES unavailable"): + await cache.get_or_load(["唤醒"], failing_loader) + + assert await cache.get(["唤醒"]) is None + + +@pytest.mark.asyncio +async def test_clear_waits_for_inflight_stage_load() -> None: + redis = _FakeRedis() + cache = SleepStageCandidateCache(redis, ttl_sec=60) + started = asyncio.Event() + release = asyncio.Event() + + async def loader(stage: str) -> list[dict[str, Any]]: + started.set() + await release.wait() + return [_doc(stage, "https://cdn/wake.mp3", [stage])] + + load_task = asyncio.create_task(cache.get_or_load(["唤醒"], loader)) + await started.wait() + clear_task = asyncio.create_task(cache.clear_all()) + await asyncio.sleep(0) + assert not clear_task.done() + + release.set() + await load_task + await clear_task + + assert redis.store == {} + + @pytest.mark.asyncio async def test_clear_all_removes_indexes_and_docs() -> None: redis = _FakeRedis() @@ -191,3 +298,14 @@ async def test_clear_all_removes_legacy_indexes_docs_and_candidate_keys() -> Non assert await redis.get("sleep_stage_index:放松") is None assert await redis.get(legacy_doc_key) is None assert await redis.get("sleep_stage_candidates:放松") is None + + +@pytest.mark.asyncio +async def test_clear_all_removes_stale_awake_stage_index() -> None: + redis = _FakeRedis() + cache = SleepStageCandidateCache(redis, ttl_sec=60) + await cache.set_stage("清醒", [_doc("旧清醒", "https://cdn/awake.mp3", ["清醒"])]) + + await cache.clear_all() + + assert await redis.get(build_sleep_stage_index_key("清醒")) is None diff --git a/tests/test_somni_audio_catalog.py b/tests/test_somni_audio_catalog.py new file mode 100644 index 0000000..d47dcc9 --- /dev/null +++ b/tests/test_somni_audio_catalog.py @@ -0,0 +1,565 @@ +"""量产音频目录查询。""" + +from __future__ import annotations + +import asyncio +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from app.core.config import Settings +from app.core.exceptions import AppError, EncoderNotReadyError +from app.server.somni.audio import catalog +from app.server.somni.audio.catalog import AudioCatalogService, InvalidAudioQueryError + + +class _Cursor: + def __init__(self, docs: list) -> None: + self._docs = list(docs) + self._index = 0 + + def skip(self, count: int) -> _Cursor: + self._docs = self._docs[count:] + return self + + def limit(self, count: int) -> _Cursor: + self._docs = self._docs[:count] + return self + + def __aiter__(self) -> _Cursor: + return self + + async def __anext__(self): + if self._index >= len(self._docs): + raise StopAsyncIteration + doc = self._docs[self._index] + self._index += 1 + return doc + + +_RAIN = { + "_id": "a1", + "audio_name": "雨夜", + "embedding": [0.1], + "content_form_tags": [ + { + "tag_id": "root-rain", + "code": "natural_sound", + "name": "自然声", + "parent_tag_id": None, + }, + { + "tag_id": "child-rain", + "code": "steady_rain", + "name": "中雨/稳定雨声", + "parent_tag_id": "root-rain", + }, + ], +} +_MUSIC = { + "_id": "a2", + "audio_name": "钢琴", + "content_form_tags": [ + { + "tag_id": "root-music", + "code": "music", + "name": "音乐", + "parent_tag_id": None, + } + ], +} + + +def _service( + collection: MagicMock, + *, + es_search: MagicMock | None = None, + encoder: MagicMock | None = None, + hot: MagicMock | None = None, + fetch_all_hard_limit: int = 50, + cache_ttl_sec: float = 60.0, +) -> AudioCatalogService: + db = MagicMock() + db.__getitem__ = MagicMock(return_value=collection) + client = MagicMock() + client.__getitem__ = MagicMock(return_value=db) + settings = Settings( + somni_mongo_db="Somni", + somni_mongo_materials_collection="somni_audio_materials", + default_page_size=1, + max_page_size=200, + fetch_all_hard_limit=fetch_all_hard_limit, + get_audio_root_tag_sim_threshold=0.75, + somni_audio_catalog_cache_ttl_sec=cache_ttl_sec, + ) + return AudioCatalogService( + client, + settings, + es_search=es_search, + encoder=encoder, + hot=hot, + ) + + +def _mongo_collection(docs: list) -> MagicMock: + collection = MagicMock() + collection.count_documents = AsyncMock(return_value=len(docs)) + collection.find = MagicMock(return_value=_Cursor(docs)) + return collection + + +@pytest.mark.asyncio +async def test_get_audio_filters_content_form_code_then_pages() -> None: + svc = _service(_mongo_collection([_RAIN, _MUSIC])) + payload = await svc.get_audio( + page=1, + page_size=1, + fetch_all=False, + query_text="", + tag_code="steady_rain", + ) + assert [item["id"] for item in payload["list"]] == ["a1"] + assert payload["total"] == 1 + assert set(payload["list"][0]) == { + "id", + "audio_name", + "audio_url", + "cover_url", + "description", + "vip", + } + assert payload["list"][0]["vip"] == 0 + + +@pytest.mark.asyncio +async def test_get_audio_uses_cache_on_second_call() -> None: + collection = _mongo_collection([_RAIN, _MUSIC]) + svc = _service(collection) + await svc.get_audio(page=1, page_size=10, fetch_all=False, query_text="", tag_code="") + await svc.get_audio(page=1, page_size=10, fetch_all=False, query_text="", tag_code="music") + assert collection.find.call_count == 1 + + +@pytest.mark.asyncio +async def test_get_audio_keeps_mongo_and_es_caches_separate() -> None: + collection = _mongo_collection([_RAIN, _MUSIC]) + encoder = MagicMock() + encoder.is_loaded = True + encoder.encode_one = AsyncMock(return_value=[1.0, 0.0]) + es_search = MagicMock() + es_search.list_audio_catalog_docs = AsyncMock(return_value=[_RAIN]) + es_search.list_content_tag_vectors = AsyncMock( + return_value=[ + { + "id": "root-rain", + "dimension": "content_form", + "parent_tag_id": "", + "vector": [1.0, 0.0], + } + ] + ) + svc = _service(collection, es_search=es_search, encoder=encoder) + + await svc.get_audio( + page=1, page_size=10, fetch_all=False, query_text="", tag_code="" + ) + payload = await svc.get_audio( + page=1, page_size=10, fetch_all=False, query_text="雨声", tag_code="" + ) + + es_search.list_audio_catalog_docs.assert_awaited_once_with(size=51) + assert [item["id"] for item in payload["list"]] == ["a1"] + + +@pytest.mark.asyncio +async def test_get_audio_cache_expires(monkeypatch) -> None: + times = iter([100.0, 111.0]) + monkeypatch.setattr(catalog, "monotonic", lambda: next(times), raising=False) + collection = _mongo_collection([_RAIN]) + svc = _service(collection, cache_ttl_sec=10.0) + + await svc.get_audio( + page=1, page_size=10, fetch_all=False, query_text="", tag_code="" + ) + await svc.get_audio( + page=1, page_size=10, fetch_all=False, query_text="", tag_code="" + ) + + assert collection.find.call_count == 2 + + +@pytest.mark.asyncio +async def test_get_audio_query_text_matches_root_tag_via_es() -> None: + collection = _mongo_collection([_RAIN]) + encoder = MagicMock() + encoder.is_loaded = True + encoder.encode_one = AsyncMock(return_value=[1.0, 0.0]) + es_search = MagicMock() + es_search.list_content_tag_vectors = AsyncMock( + return_value=[ + { + "id": "root-rain", + "dimension": "content_form", + "parent_tag_id": "", + "vector": [1.0, 0.0], + }, + { + "id": "child-rain", + "dimension": "content_form", + "parent_tag_id": "root-rain", + "vector": [1.0, 0.0], + }, + ] + ) + es_search.list_audio_catalog_docs = AsyncMock(return_value=[_RAIN, _MUSIC]) + svc = _service(collection, es_search=es_search, encoder=encoder) + payload = await svc.get_audio( + page=1, + page_size=10, + fetch_all=False, + query_text="雨声", + tag_code="", + ) + collection.find.assert_not_called() + es_search.list_audio_catalog_docs.assert_awaited_once_with(size=51) + assert [item["id"] for item in payload["list"]] == ["a1"] + + +@pytest.mark.asyncio +async def test_get_audio_query_text_matches_child_tag() -> None: + """搜索词更接近二级标签(如「白噪音」)时应按子标签命中,而非仅根标签。""" + white_noise = { + "_id": "a3", + "audio_name": "白噪音", + "content_form_tags": [ + { + "tag_id": "root-color-noise", + "code": "color_noise", + "name": "颜色噪音", + "parent_tag_id": None, + }, + { + "tag_id": "child-white-noise", + "code": "white_noise", + "name": "白噪音", + "parent_tag_id": "root-color-noise", + }, + ], + } + encoder = MagicMock() + encoder.is_loaded = True + encoder.encode_one = AsyncMock(return_value=[1.0, 0.0]) + es_search = MagicMock() + es_search.list_content_tag_vectors = AsyncMock( + return_value=[ + { + "id": "root-color-noise", + "dimension": "content_form", + "parent_tag_id": "", + "vector": [0.0, 1.0], # 与查询正交,根标签不命中 + }, + { + "id": "child-white-noise", + "dimension": "content_form", + "parent_tag_id": "root-color-noise", + "vector": [1.0, 0.0], # 子标签命中 + }, + ] + ) + es_search.list_audio_catalog_docs = AsyncMock(return_value=[_RAIN, white_noise]) + svc = _service( + _mongo_collection([white_noise]), + es_search=es_search, + encoder=encoder, + ) + payload = await svc.get_audio( + page=1, + page_size=10, + fetch_all=False, + query_text="白噪音", + tag_code="", + ) + assert [item["id"] for item in payload["list"]] == ["a3"] + + +@pytest.mark.asyncio +async def test_get_audio_query_text_returns_empty_when_child_unused() -> None: + """词典子标签命中但物料未挂该子标签时,直接返回空列表,不回退父级。""" + pink = { + "_id": "a4", + "audio_name": "粉噪音", + "content_form_tags": [ + { + "tag_id": "root-color-noise", + "code": "color_noise", + "name": "颜色噪音", + "parent_tag_id": None, + }, + { + "tag_id": "child-pink-noise", + "code": "pink_noise", + "name": "粉噪音", + "parent_tag_id": "root-color-noise", + }, + ], + } + encoder = MagicMock() + encoder.is_loaded = True + encoder.encode_one = AsyncMock(return_value=[1.0, 0.0]) + es_search = MagicMock() + es_search.list_content_tag_vectors = AsyncMock( + return_value=[ + { + "id": "root-color-noise", + "dimension": "content_form", + "parent_tag_id": "", + "vector": [0.2, 0.8], + }, + { + "id": "child-white-noise", + "dimension": "content_form", + "parent_tag_id": "root-color-noise", + "vector": [1.0, 0.0], + }, + ] + ) + es_search.list_audio_catalog_docs = AsyncMock(return_value=[_RAIN, pink]) + svc = _service(_mongo_collection([pink]), es_search=es_search, encoder=encoder) + payload = await svc.get_audio( + page=1, + page_size=10, + fetch_all=False, + query_text="白噪音", + tag_code="", + ) + assert payload["list"] == [] + assert payload["total"] == 0 + + +@pytest.mark.asyncio +async def test_get_audio_records_hot_when_query() -> None: + encoder = MagicMock() + encoder.is_loaded = True + encoder.encode_one = AsyncMock(return_value=[1.0, 0.0]) + es_search = MagicMock() + es_search.list_content_tag_vectors = AsyncMock( + return_value=[ + { + "id": "root-rain", + "dimension": "content_form", + "parent_tag_id": "", + "vector": [1.0, 0.0], + } + ] + ) + es_search.list_audio_catalog_docs = AsyncMock(return_value=[_RAIN]) + hot = MagicMock() + hot.record_search = AsyncMock() + svc = _service( + _mongo_collection([_RAIN]), + es_search=es_search, + encoder=encoder, + hot=hot, + ) + + await svc.get_audio( + page=1, + page_size=10, + fetch_all=False, + query_text=" 雨声 ", + tag_code="", + ) + await asyncio.sleep(0) + + hot.record_search.assert_awaited_once_with(" 雨声 ", hit_count=1) + + +@pytest.mark.asyncio +async def test_get_audio_succeeds_when_hot_recording_fails() -> None: + encoder = MagicMock() + encoder.is_loaded = True + encoder.encode_one = AsyncMock(return_value=[1.0, 0.0]) + es_search = MagicMock() + es_search.list_content_tag_vectors = AsyncMock( + return_value=[ + { + "id": "root-rain", + "dimension": "content_form", + "parent_tag_id": "", + "vector": [1.0, 0.0], + } + ] + ) + es_search.list_audio_catalog_docs = AsyncMock(return_value=[_RAIN]) + hot = MagicMock() + hot.record_search = AsyncMock(side_effect=RuntimeError("hot unavailable")) + svc = _service( + _mongo_collection([_RAIN]), + es_search=es_search, + encoder=encoder, + hot=hot, + ) + + payload = await svc.get_audio( + page=1, + page_size=10, + fetch_all=False, + query_text="雨声", + tag_code="", + ) + await asyncio.sleep(0) + + assert [item["id"] for item in payload["list"]] == ["a1"] + hot.record_search.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_get_audio_query_text_without_encoder_fails() -> None: + es_search = MagicMock() + es_search.list_audio_catalog_docs = AsyncMock(return_value=[_RAIN]) + svc = _service(_mongo_collection([_RAIN]), es_search=es_search) + with pytest.raises(EncoderNotReadyError): + await svc.get_audio( + page=1, + page_size=10, + fetch_all=False, + query_text="雨声", + tag_code="", + ) + + +@pytest.mark.asyncio +async def test_get_audio_rejects_invalid_page() -> None: + svc = _service(_mongo_collection([])) + with pytest.raises(InvalidAudioQueryError): + await svc.get_audio( + page=0, + page_size=20, + fetch_all=False, + query_text="", + tag_code="", + ) + + +@pytest.mark.asyncio +async def test_get_audio_mongo_load_rejects_over_limit() -> None: + collection = MagicMock() + collection.count_documents = AsyncMock(return_value=6) + svc = _service(collection, fetch_all_hard_limit=5) + with pytest.raises(InvalidAudioQueryError): + await svc.get_audio( + page=None, + page_size=None, + fetch_all=False, + query_text="", + tag_code="", + ) + + +@pytest.mark.asyncio +async def test_get_audio_tag_requires_mongo() -> None: + svc = AudioCatalogService(None, Settings()) + with pytest.raises(AppError) as exc: + await svc.get_audio_tag() + assert exc.value.status_code == 503 + + +@pytest.mark.asyncio +async def test_get_audio_tag_maps_root_fields() -> None: + collection = MagicMock() + collection.count_documents = AsyncMock(return_value=1) + collection.find = MagicMock( + return_value=_Cursor( + [ + { + "_id": "root-natural", + "type": "content_form", + "code": "natural_sound", + "name": "自然声", + "name_en": "Natural Sound", + "parent_tag_id": None, + "status": "启用", + } + ] + ) + ) + svc = _service(collection) + payload = await svc.get_audio_tag() + assert payload["tags"][0] == { + "type": "content_form", + "code": "natural_sound", + "name": "自然声", + "name_en": "Natural Sound", + "id": "root-natural", + "parent_tag_id": None, + "status": "启用", + } + query = catalog._root_tag_query() + assert query["type"] == "content_form" + assert query["status"] == "启用" + collection.find.assert_called_once_with( + query, + { + "_id": 1, + "id": 1, + "type": 1, + "code": 1, + "name": 1, + "name_en": 1, + "parent_tag_id": 1, + "status": 1, + }, + ) + + +def test_root_tag_query_only_content_form() -> None: + query = catalog._root_tag_query() + assert query["type"] == "content_form" + + +def test_to_vip_normalizes_bool_int_and_string() -> None: + assert catalog._to_vip(None) == 0 + assert catalog._to_vip(False) == 0 + assert catalog._to_vip(0) == 0 + assert catalog._to_vip("false") == 0 + assert catalog._to_vip("0") == 0 + assert catalog._to_vip(True) == 1 + assert catalog._to_vip(1) == 1 + assert catalog._to_vip("true") == 1 + assert catalog._to_vip("1") == 1 + + +@pytest.mark.asyncio +async def test_get_audio_maps_vip_true_to_one() -> None: + doc = {**_RAIN, "vip": True} + svc = _service(_mongo_collection([doc])) + payload = await svc.get_audio( + page=1, + page_size=10, + fetch_all=False, + query_text="", + tag_code="", + ) + assert payload["list"][0]["vip"] == 1 + + +@pytest.mark.asyncio +async def test_drain_hot_tasks_waits_for_pending_recording() -> None: + started = asyncio.Event() + release = asyncio.Event() + + async def _slow_record(*_args, **_kwargs): + started.set() + await release.wait() + + hot = MagicMock() + hot.record_search = AsyncMock(side_effect=_slow_record) + svc = _service(_mongo_collection([_RAIN]), hot=hot) + svc._schedule_hot("雨声", 1) + + await started.wait() + release.set() + await svc.drain_hot_tasks(timeout_sec=1.0) + + assert not svc._hot_tasks + hot.record_search.assert_awaited_once_with("雨声", hit_count=1) diff --git a/tests/test_somni_audio_hot.py b/tests/test_somni_audio_hot.py new file mode 100644 index 0000000..0e6b0bd --- /dev/null +++ b/tests/test_somni_audio_hot.py @@ -0,0 +1,248 @@ +import asyncio +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from app.core.config import Settings +from app.es.search_events import SearchEventsStore + + +def test_resolve_somni_redis_url_empty_does_not_fall_back() -> None: + from app.core.somni_redis import resolve_somni_redis_url + + settings = Settings(somni_redis_url=" ", redis_url="redis://localhost:6379/0") + assert resolve_somni_redis_url(settings) == "" + + +def test_resolve_somni_redis_url_prefers_somni() -> None: + from app.core.somni_redis import resolve_somni_redis_url + + settings = Settings( + somni_redis_url="redis://somni:6379/1", + redis_url="redis://localhost:6379/0", + ) + assert resolve_somni_redis_url(settings) == "redis://somni:6379/1" + + +@pytest.mark.asyncio +async def test_create_somni_redis_uses_only_somni_config(monkeypatch) -> None: + from app.core.somni_redis import create_somni_redis + + client = MagicMock() + client.ping = AsyncMock() + factory = MagicMock(return_value=client) + monkeypatch.setattr("app.core.somni_redis.Redis.from_url", factory) + settings = Settings( + somni_redis_url="redis://somni:6379/0", + redis_url="redis://shared:6379/0", + somni_redis_max_connections=64, + somni_redis_connect_timeout_sec=1.5, + somni_redis_socket_timeout_sec=2.5, + ) + + assert await create_somni_redis(settings) is client + + factory.assert_called_once_with( + "redis://somni:6379/0", + decode_responses=True, + max_connections=64, + socket_connect_timeout=1.5, + socket_timeout=2.5, + health_check_interval=30, + ) + client.ping.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_create_somni_redis_closes_failed_client(monkeypatch) -> None: + from app.core.somni_redis import create_somni_redis + + client = MagicMock() + client.ping = AsyncMock(side_effect=ConnectionError("unavailable")) + client.aclose = AsyncMock() + monkeypatch.setattr("app.core.somni_redis.Redis.from_url", MagicMock(return_value=client)) + + with pytest.raises(ConnectionError): + await create_somni_redis(Settings(somni_redis_url="redis://somni:6379/0")) + + client.aclose.assert_awaited_once() + + +def test_hot_settings_defaults() -> None: + s = Settings() + assert s.somni_hot_enabled is True + assert s.somni_hot_top_n == 10 + assert s.somni_hot_redis_key == "somni:audio:hot:v1" + assert s.somni_redis_max_connections == 128 + assert s.somni_redis_connect_timeout_sec == 2.0 + assert s.somni_redis_socket_timeout_sec == 2.0 + assert s.somni_es_search_events_index == "somni_audio_search_events" + + +@pytest.mark.asyncio +async def test_search_events_ensure_and_index() -> None: + client = MagicMock() + client.indices.exists = AsyncMock(return_value=False) + client.indices.create = AsyncMock() + client.index = AsyncMock() + store = SearchEventsStore(client, Settings()) + await store.ensure_index() + client.indices.create.assert_awaited() + await store.index_event(keyword="雨声", raw_query=" 雨声 ", hit_count=2) + client.indices.exists.assert_awaited_once() + client.indices.create.assert_awaited_once() + kwargs = client.index.await_args.kwargs + assert kwargs["index"] == "somni_audio_search_events" + assert kwargs["document"]["keyword"] == "雨声" + assert kwargs["document"]["hit_count"] == 2 + + +@pytest.mark.asyncio +async def test_search_events_already_exists_still_indexes_and_caches_ensure() -> None: + class _AlreadyExistsError(Exception): + error = "resource_already_exists_exception" + + client = MagicMock() + client.indices.exists = AsyncMock(return_value=False) + client.indices.create = AsyncMock(side_effect=_AlreadyExistsError()) + client.index = AsyncMock() + store = SearchEventsStore(client, Settings()) + + await asyncio.gather( + store.index_event(keyword="雨声", raw_query=" 雨声 ", hit_count=1), + store.index_event(keyword="风声", raw_query="风声", hit_count=2), + ) + + client.indices.exists.assert_awaited_once() + client.indices.create.assert_awaited_once() + assert client.index.await_count == 2 + + +def test_normalize_keyword_strips() -> None: + from app.server.somni.audio.hot import normalize_keyword + + assert normalize_keyword(" 雨声 ") == "雨声" + assert normalize_keyword(" ") == "" + + +@pytest.mark.asyncio +async def test_record_and_list_hot() -> None: + from app.server.somni.audio.hot import HotTracker + + redis = MagicMock() + redis.zincrby = AsyncMock() + redis.zrevrange = AsyncMock( + return_value=[("雨声".encode(), 2.0), ("暴雨声".encode(), 1.0)] + ) + events = MagicMock() + events.index_event = AsyncMock() + tracker = HotTracker(redis, events, Settings()) + await tracker.record_search(" 雨声 ", hit_count=3) + redis.zincrby.assert_awaited_once() + events.index_event.assert_awaited_once() + items = await tracker.list_hot() + assert items == [{"keyword": "雨声", "score": 2}, {"keyword": "暴雨声", "score": 1}] + + +@pytest.mark.asyncio +async def test_record_blank_skipped() -> None: + from app.server.somni.audio.hot import HotTracker + + redis = MagicMock() + redis.zincrby = AsyncMock() + events = MagicMock() + events.index_event = AsyncMock() + tracker = HotTracker(redis, events, Settings()) + await tracker.record_search(" ", hit_count=0) + redis.zincrby.assert_not_called() + events.index_event.assert_not_called() + + +@pytest.mark.asyncio +async def test_record_search_redis_failure_still_indexes_es() -> None: + from app.server.somni.audio.hot import HotTracker + + redis = MagicMock() + redis.zincrby = AsyncMock(side_effect=RuntimeError("redis unavailable")) + events = MagicMock() + events.index_event = AsyncMock() + tracker = HotTracker(redis, events, Settings()) + + await tracker.record_search(" 雨声 ", hit_count=3) + + events.index_event.assert_awaited_once_with( + keyword="雨声", + raw_query=" 雨声 ", + hit_count=3, + ) + + +@pytest.mark.asyncio +async def test_record_search_es_failure_returns_after_redis_increment() -> None: + from app.server.somni.audio.hot import HotTracker + + redis = MagicMock() + redis.zincrby = AsyncMock() + events = MagicMock() + events.index_event = AsyncMock(side_effect=RuntimeError("es unavailable")) + tracker = HotTracker(redis, events, Settings()) + + await tracker.record_search("雨声", hit_count=1) + + redis.zincrby.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_hot_disabled_skips_writes_and_returns_empty_list() -> None: + from app.server.somni.audio.hot import HotTracker + + redis = MagicMock() + redis.zincrby = AsyncMock() + redis.zrevrange = AsyncMock() + events = MagicMock() + events.index_event = AsyncMock() + tracker = HotTracker(redis, events, Settings(somni_hot_enabled=False)) + + await tracker.record_search("雨声", hit_count=1) + assert await tracker.list_hot() == [] + + redis.zincrby.assert_not_called() + redis.zrevrange.assert_not_called() + events.index_event.assert_not_called() + + +@pytest.mark.asyncio +async def test_list_hot_non_positive_top_n_returns_empty_without_redis_read() -> None: + from app.server.somni.audio.hot import HotTracker + + redis = MagicMock() + redis.zrevrange = AsyncMock() + tracker = HotTracker(redis, None, Settings(somni_hot_top_n=0)) + + assert await tracker.list_hot() == [] + redis.zrevrange.assert_not_called() + + +@pytest.mark.asyncio +async def test_list_hot_requires_redis() -> None: + from app.core.exceptions import AppError + from app.server.somni.audio.hot import HotTracker + + tracker = HotTracker(None, None, Settings()) + with pytest.raises(AppError): + await tracker.list_hot() + + +@pytest.mark.asyncio +async def test_list_hot_redis_failure_is_service_unavailable() -> None: + from app.core.exceptions import AppError + from app.server.somni.audio.hot import HotTracker + + redis = MagicMock() + redis.zrevrange = AsyncMock(side_effect=ConnectionError("unavailable")) + tracker = HotTracker(redis, None, Settings()) + + with pytest.raises(AppError) as exc: + await tracker.list_hot() + + assert exc.value.status_code == 503 diff --git a/tests/test_somni_report_calc.py b/tests/test_somni_report_calc.py new file mode 100644 index 0000000..2b3b0ad --- /dev/null +++ b/tests/test_somni_report_calc.py @@ -0,0 +1,61 @@ +"""量产报告 calc 单测。""" + +from __future__ import annotations + +from datetime import UTC, datetime + +from app.server.somni.report.calc import ( + floor_avg, + floor_metric_stats, + format_hhmm, + local_day_utc_range, + minutes_between, + sleep_stage_parts, + stage_minutes, +) + + +def test_local_day_utc_range_shanghai() -> None: + start, end = local_day_utc_range("2026-08-10") + assert start == datetime(2026, 8, 9, 16, 0, tzinfo=UTC) + assert end == datetime(2026, 8, 10, 16, 0, tzinfo=UTC) + + +def test_minutes_between_and_stage() -> None: + bed = datetime(2026, 8, 9, 15, 30, tzinfo=UTC) # 23:30 CST + wake = datetime(2026, 8, 10, 0, 30, tzinfo=UTC) # 08:30 CST + assert minutes_between(bed, wake) == 540 + assert stage_minutes(540, 17) == 91 + assert stage_minutes(0, 17) == 0 + + +def test_floor_avg_and_metric_stats() -> None: + assert floor_avg([66.5, 62.2]) == 64 + assert floor_avg([]) == 0 + assert floor_metric_stats([21.9, 23.2, 25.8]) == { + "value": 23, + "min": 21, + "max": 25, + } + assert floor_metric_stats([]) == {"value": 0, "min": 0, "max": 0} + + +def test_format_hhmm() -> None: + assert format_hhmm(datetime(2026, 8, 9, 15, 30, tzinfo=UTC)) == "23:30" + assert format_hhmm(None) == "" + + +def test_sleep_stage_parts_excludes_awake_from_total() -> None: + raw = { + "bed_time": datetime(2026, 8, 9, 15, 30, tzinfo=UTC), + "wake_up_time": datetime(2026, 8, 10, 0, 30, tzinfo=UTC), + "awake_ratio": 10, + "deep_sleep_ratio": 20, + "light_sleep_ratio": 50, + "rem_ratio": 20, + } + parts = sleep_stage_parts(raw) + assert parts["bed_minutes"] == 540 + assert parts["deep_sleep"]["minutes"] == 108 + assert parts["total_minutes"] == 108 + 270 + 108 + assert parts["awake"]["percent"] == 10 diff --git a/tests/test_somni_report_service.py b/tests/test_somni_report_service.py new file mode 100644 index 0000000..0d375b2 --- /dev/null +++ b/tests/test_somni_report_service.py @@ -0,0 +1,198 @@ +"""量产 ReportService 业务单测。""" + +from __future__ import annotations + +from datetime import UTC, datetime +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from app.core.config import Settings +from app.core.exceptions import MongoNotConfiguredError +from app.server.somni.report.service import ReportService + + +def _settings() -> Settings: + return Settings(somni_mongo_uri="mongodb://localhost") + + +def _service(store: MagicMock) -> ReportService: + return ReportService(client=MagicMock(), settings=_settings(), store=store) + + +@pytest.mark.asyncio +async def test_get_summary_computes_stages_and_avg() -> None: + store = MagicMock() + store.find_record = AsyncMock( + return_value={ + "raw_data": { + "bed_time": datetime(2026, 8, 9, 15, 30, tzinfo=UTC), + "wake_up_time": datetime(2026, 8, 10, 0, 30, tzinfo=UTC), + "deep_sleep_ratio": 20, + "light_sleep_ratio": 50, + "rem_ratio": 20, + "awake_ratio": 10, + } + } + ) + store.find_sleep_report = AsyncMock( + return_value={ + "sleep_summary": { + "body_battery": 90, + "body_battery_status": "Energy At Its Peak", + } + } + ) + store.find_device_id = AsyncMock(return_value="dev_1") + store.list_telemetry = AsyncMock( + return_value=[ + {"data": {"hr": 66.5, "br": 16}}, + {"data": {"hr": 62.2, "br": 14.8}}, + ] + ) + svc = _service(store) + payload = await svc.get_summary("u1", "2026-08-10") + summary = payload["sleep_summary"] + assert summary["body_battery"] == 90 + assert summary["body_battery_status"] == "Energy At Its Peak" + assert summary["deep_sleep_minutes"] == 108 + assert summary["total_minutes"] == 108 + 270 + 108 + assert summary["avg_heart_rate"] == 64 + assert summary["avg_respiratory_rate"] == 15 + + +@pytest.mark.asyncio +async def test_get_summary_without_device_zeros_avg() -> None: + store = MagicMock() + store.find_record = AsyncMock(return_value=None) + store.find_sleep_report = AsyncMock(return_value=None) + store.find_device_id = AsyncMock(return_value=None) + svc = _service(store) + summary = (await svc.get_summary("u1", "2026-08-10"))["sleep_summary"] + assert summary == { + "body_battery": 0, + "body_battery_status": "", + "total_minutes": 0, + "deep_sleep_minutes": 0, + "avg_heart_rate": 0, + "avg_respiratory_rate": 0, + } + + +@pytest.mark.asyncio +async def test_get_environment_stats() -> None: + store = MagicMock() + store.find_device_id = AsyncMock(return_value="dev_1") + store.list_telemetry = AsyncMock( + return_value=[ + {"data": {"temp": 21.9, "humi": 40.2, "lux": 1.2, "noise_db": 28.9}}, + {"data": {"temp": 25.8, "humi": 55.1, "lux": 4.9, "noise_db": 34.1}}, + ] + ) + env = (await _service(store).get_environment("u1", "2026-08-10"))[ + "environment_summary" + ] + assert env["temperature"] == {"value": 23, "min": 21, "max": 25} + assert env["noise"]["value"] == 31 + assert "status" not in env["temperature"] + + +@pytest.mark.asyncio +async def test_get_events_attaches_intervention() -> None: + store = MagicMock() + store.list_events = AsyncMock( + return_value=[ + { + "_id": "ab1", + "event_time": "02:15", + "type": "abnormal", + "code": "heart_rate_increase", + "events": [ + { + "event_type": "心率上升", + "duration": "00:03:20", + "trigger_cause": "可能受到噪音影响", + "action_taken": "播放舒缓声音", + "result_summary": "心率恢复", + } + ], + }, + { + "_id": "iv1", + "type": "intervention", + "related_event_id": "ab1", + "event_time": "02:16", + "event_type": "干预", + "duration": "00:01:00", + "trigger_cause": "噪音", + "action_taken": "播放白噪音", + "result_summary": "恢复", + }, + ] + ) + store.find_record = AsyncMock( + return_value={"idf_data": [{"stage": "deep", "start": "01:00", "end": "02:20"}]} + ) + store.find_device_id = AsyncMock(return_value="dev_1") + store.list_telemetry = AsyncMock( + side_effect=[ + [ + { + "ts": datetime(2026, 8, 9, 18, 15, tzinfo=UTC), + "data": {"hr": 68.2, "br": 16.1}, + } + ], + [ + { + "ts": datetime(2026, 8, 9, 18, 15, tzinfo=UTC), + "data": {"temp": 23.2, "humi": 52.9, "lux": 1.2, "noise_db": 28.4}, + } + ], + ] + ) + payload = await _service(store).get_events("u1", "2026-08-10") + assert payload["event_count"] == 1 + assert payload["abnormal_count"] == 1 + assert payload["intervention_count"] == 1 + assert len(payload["sleep_events"]) == 1 + assert payload["sleep_events"][0]["intervention"]["event_type"] == "干预" + assert payload["idf_data"][0]["stage"] == "deep" + assert payload["physio_data"][0]["metrics"]["heart_rate"] == 68 + assert payload["env_data"][0]["noise"] == 28 + + +@pytest.mark.asyncio +async def test_get_structure_and_sleep_quality() -> None: + store = MagicMock() + store.find_record = AsyncMock( + return_value={ + "raw_data": { + "bed_time": datetime(2026, 8, 9, 15, 30, tzinfo=UTC), + "sleep_time": datetime(2026, 8, 9, 15, 50, tzinfo=UTC), + "wake_time": datetime(2026, 8, 10, 0, 12, tzinfo=UTC), + "wake_up_time": datetime(2026, 8, 10, 0, 30, tzinfo=UTC), + "awake_ratio": 10, + "deep_sleep_ratio": 20, + "light_sleep_ratio": 50, + "rem_ratio": 20, + "sleep_latency": 20, + "sleep_efficiency": 92, + } + } + ) + svc = _service(store) + structure = (await svc.get_structure("u1", "2026-08-10"))["sleep_structure"] + assert structure["deep_sleep"] == {"minutes": 108, "percent": 20} + quality = (await svc.get_sleep_quality("u1", "2026-08-10"))["sleep_quality"] + assert quality["time_in_bed_minutes"] == 540 + assert quality["bedtime"] == "23:30" + assert quality["wake_up_time"] == "08:30" + assert quality["awake_after_onset_minutes"] == 18 + assert quality["sleep_onset_latency_minutes"] == 20 + + +@pytest.mark.asyncio +async def test_mongo_not_configured() -> None: + svc = ReportService(client=None, settings=_settings()) + with pytest.raises(MongoNotConfiguredError): + await svc.get_summary("u1", "2026-08-10") diff --git a/tests/test_somni_report_store.py b/tests/test_somni_report_store.py new file mode 100644 index 0000000..f22e69d --- /dev/null +++ b/tests/test_somni_report_store.py @@ -0,0 +1,45 @@ +"""ReportStore 查询行为。""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from app.core.config import Settings +from app.server.somni.report.store import ReportStore + + +@pytest.mark.asyncio +async def test_list_events_reads_sleep_events_field() -> None: + collection = MagicMock() + collection.find_one = AsyncMock( + return_value={ + "uid": "u1", + "record_date": "2026-08-10", + "sleep_events": [ + {"type": "abnormal", "_id": "a1"}, + {"type": "intervention", "related_event_id": "a1"}, + ], + } + ) + db = MagicMock() + db.__getitem__.return_value = collection + client = MagicMock() + client.__getitem__.return_value = db + store = ReportStore(client, Settings()) + events = await store.list_events("u1", "2026-08-10") + assert len(events) == 2 + assert events[0]["type"] == "abnormal" + + +@pytest.mark.asyncio +async def test_list_events_empty_when_missing() -> None: + collection = MagicMock() + collection.find_one = AsyncMock(return_value=None) + db = MagicMock() + db.__getitem__.return_value = collection + client = MagicMock() + client.__getitem__.return_value = db + store = ReportStore(client, Settings()) + assert await store.list_events("u1", "2026-08-10") == [] diff --git a/tests/test_sync_es_from_comm.py b/tests/test_sync_es_from_comm.py index e48fa57..f621721 100644 --- a/tests/test_sync_es_from_comm.py +++ b/tests/test_sync_es_from_comm.py @@ -1,4 +1,4 @@ -"""scripts/sync_es_from_comm.py Mongo 同步逻辑单元测试。""" +"""scripts/sync_es_from_comm.py Mongo 全量重建同步单元测试。""" from __future__ import annotations @@ -17,11 +17,10 @@ _redact_mongo_uri, bson_to_jsonable, material_doc_to_es, - material_documents_differ, mongo_doc_id, start_sync_scheduler, - tag_dictionary_compare_snapshot, - tag_documents_differ, + tag_doc_to_es, + wipe_and_recreate_index, zero_vector, ) @@ -42,6 +41,7 @@ def _material_doc( ) -> dict: return { "_id": ObjectId(doc_id) if len(doc_id) == 24 else doc_id, + "id": doc_id, "audio_name": audio_name, "description": "描述", "status": True, @@ -63,6 +63,7 @@ def _material_doc( def _tag_doc(doc_id: str, *, name: str = "放松", name_en: str = "Unwind") -> dict: return { "_id": ObjectId(doc_id) if len(doc_id) == 24 else doc_id, + "id": doc_id, "type": "sleep_stage", "code": "unwind", "status": "启用", @@ -87,38 +88,52 @@ def test_material_doc_to_es_requires_audio_url() -> None: assert material_doc_to_es(doc) is None -def test_tag_documents_same_when_only_vectors_differ() -> None: - desired = {"name": "放松", "name_en": "Unwind", "status": "启用"} - existing = { - "name": "放松", - "name_en": "Unwind", - "status": "启用", - "name_vector": [0.1], - "name_en_vector": [0.2], - } - assert tag_documents_differ(desired, existing) is False +def test_material_doc_to_es_keeps_sleep_stage_names_without_id_fields() -> None: + payload = material_doc_to_es(_material_doc("6a33a7928030d4cf420efeb6")) + assert payload is not None + assert "id" not in payload + assert "_id" not in payload + assert payload["sleep_stage_names"] == ["放松"] + + +def test_tag_doc_to_es_strips_id_fields() -> None: + payload = tag_doc_to_es(_tag_doc("6a325acc1a3dbc128504c423")) + assert payload is not None + assert "id" not in payload + assert "_id" not in payload + assert payload["name"] == "放松" + + +@pytest.mark.asyncio +async def test_wipe_and_recreate_index_deletes_then_ensures() -> None: + es_client = MagicMock() + es_client.indices.exists = AsyncMock(return_value=True) + es_client.count = AsyncMock(return_value={"count": 12}) + es_client.indices.delete = AsyncMock() + es_search = MagicMock() + es_search.ensure_indices = AsyncMock() + deleted = await wipe_and_recreate_index(es_client, es_search, "somni_audio_materials") -def test_material_documents_differ_on_tag_change() -> None: - desired = material_doc_to_es(_material_doc("6a33a7928030d4cf420efeb6")) - existing = dict(desired) - existing["sleep_stage_tags"] = [] - assert material_documents_differ(desired, existing) is True + assert deleted == 12 + es_client.indices.delete.assert_awaited_once_with(index="somni_audio_materials") + es_search.ensure_indices.assert_awaited_once() @pytest.mark.asyncio -async def test_tag_sync_job_deletes_es_orphan() -> None: +async def test_tag_sync_job_wipes_index_then_inserts() -> None: mongo = MagicMock() mongo.fetch_tag_dictionary = AsyncMock(return_value=[_tag_doc("6a325acc1a3dbc128504c423")]) es_search = MagicMock() - es_search.list_all_tag_dictionary_doc_ids = AsyncMock( - return_value={"6a325acc1a3dbc128504c423", "orphan"} - ) - es_search.get_tag_dictionary_source = AsyncMock(return_value=None) + es_search.list_all_tag_dictionary_doc_ids = AsyncMock(return_value={"orphan"}) es_search.tag_dictionary_index = "somni_audio_tag_dictionary" + es_search.clear_content_tag_vectors_cache = MagicMock() + es_search.ensure_indices = AsyncMock() es_client = MagicMock() + es_client.indices.exists = AsyncMock(return_value=True) + es_client.count = AsyncMock(return_value={"count": 1}) + es_client.indices.delete = AsyncMock() es_client.index = AsyncMock() - es_client.delete = AsyncMock() encoder = MagicMock() encoder.encode_one = AsyncMock(return_value=[0.1] * 512) @@ -127,23 +142,27 @@ async def test_tag_sync_job_deletes_es_orphan() -> None: ).run(dry_run=False) assert stats["deleted"] == 1 - es_client.delete.assert_awaited_once_with( - index="somni_audio_tag_dictionary", id="orphan" - ) + assert stats["created"] == 1 + es_client.indices.delete.assert_awaited_once_with(index="somni_audio_tag_dictionary") + kwargs = es_client.index.await_args.kwargs + assert kwargs["id"] == "6a325acc1a3dbc128504c423" + assert "id" not in kwargs["document"] + assert "_id" not in kwargs["document"] @pytest.mark.asyncio -async def test_material_sync_job_skips_unchanged(tmp_path) -> None: +async def test_material_sync_job_wipes_then_reindexes(tmp_path) -> None: doc = _material_doc("6a33a7928030d4cf420efeb6") - es_payload = material_doc_to_es(doc) - es_payload["description_vector"] = [0.1] * 512 mongo = MagicMock() mongo.fetch_materials = AsyncMock(return_value=[doc]) es_search = MagicMock() es_search.list_all_audio_doc_ids = AsyncMock(return_value={"6a33a7928030d4cf420efeb6"}) - es_search.get_audio_source = AsyncMock(return_value=es_payload) es_search.audio_index = "somni_audio_materials" + es_search.ensure_indices = AsyncMock() es_client = MagicMock() + es_client.indices.exists = AsyncMock(return_value=True) + es_client.count = AsyncMock(return_value={"count": 99}) + es_client.indices.delete = AsyncMock() es_client.index = AsyncMock() encoder = MagicMock() encoder.encode_one = AsyncMock(return_value=[0.1] * 512) @@ -152,8 +171,14 @@ async def test_material_sync_job_skips_unchanged(tmp_path) -> None: mongo, es_search, es_client, encoder, Settings(sync_backup_dir=str(tmp_path)) ).run(dry_run=False) - assert stats["unchanged"] == 1 - es_client.index.assert_not_called() + assert stats["deleted"] == 99 + assert stats["created"] == 1 + es_client.indices.delete.assert_awaited_once_with(index="somni_audio_materials") + indexed = es_client.index.await_args.kwargs + assert indexed["id"] == "6a33a7928030d4cf420efeb6" + assert "id" not in indexed["document"] + assert "_id" not in indexed["document"] + assert indexed["document"]["sleep_stage_names"] == ["放松"] @pytest.mark.asyncio @@ -163,9 +188,11 @@ async def test_material_sync_job_writes_description_vector(tmp_path) -> None: mongo.fetch_materials = AsyncMock(return_value=[doc]) es_search = MagicMock() es_search.list_all_audio_doc_ids = AsyncMock(return_value=set()) - es_search.get_audio_source = AsyncMock(return_value=None) es_search.audio_index = "somni_audio_materials" + es_search.ensure_indices = AsyncMock() es_client = MagicMock() + es_client.indices.exists = AsyncMock(return_value=False) + es_client.indices.delete = AsyncMock() es_client.index = AsyncMock() encoder = MagicMock() encoder.encode_one = AsyncMock(return_value=[0.2] * 512) @@ -181,6 +208,29 @@ async def test_material_sync_job_writes_description_vector(tmp_path) -> None: encoder.encode_one.assert_awaited_once_with(indexed["description_text"]) +@pytest.mark.asyncio +async def test_material_sync_dry_run_does_not_wipe(tmp_path) -> None: + doc = _material_doc("6a33a7928030d4cf420efeb6") + mongo = MagicMock() + mongo.fetch_materials = AsyncMock(return_value=[doc]) + es_search = MagicMock() + es_search.list_all_audio_doc_ids = AsyncMock(return_value={"a", "b", "c"}) + es_search.audio_index = "somni_audio_materials" + es_client = MagicMock() + es_client.indices.delete = AsyncMock() + es_client.index = AsyncMock() + encoder = MagicMock() + + stats = await MaterialsSyncJob( + mongo, es_search, es_client, encoder, Settings(sync_backup_dir=str(tmp_path)) + ).run(dry_run=True) + + assert stats["deleted"] == 3 + assert stats["created"] == 1 + es_client.indices.delete.assert_not_called() + es_client.index.assert_not_called() + + @pytest.mark.asyncio async def test_mongo_sync_job_migrates_legacy_indices() -> None: mongo = MagicMock() @@ -191,7 +241,11 @@ async def test_mongo_sync_job_migrates_legacy_indices() -> None: es_search.ensure_indices = AsyncMock() es_search.list_all_tag_dictionary_doc_ids = AsyncMock(return_value=set()) es_search.list_all_audio_doc_ids = AsyncMock(return_value=set()) + es_search.tag_dictionary_index = "somni_audio_tag_dictionary" + es_search.audio_index = "somni_audio_materials" + es_search.clear_content_tag_vectors_cache = MagicMock() es_client = MagicMock() + es_client.indices.exists = AsyncMock(return_value=False) encoder = MagicMock() await MongoEsSyncJob(mongo, es_search, es_client, encoder, Settings()).run(dry_run=True) @@ -205,14 +259,6 @@ def test_zero_vector_has_embedding_dim_length() -> None: assert all(v == 0.0 for v in zero_vector(512)) -def test_tag_dictionary_snapshot_uses_created_by_fields() -> None: - doc = _tag_doc("6a325acc1a3dbc128504c423") - snapshot = tag_dictionary_compare_snapshot(bson_to_jsonable(doc)) - assert snapshot["created_by"] == "tester" - assert snapshot["updated_by"] == "tester" - assert "create_by" not in snapshot - - def test_start_sync_scheduler_skipped_without_mongo_uri() -> None: with patch("scripts.sync_es_from_comm.AsyncIOScheduler") as mock_cls: settings = Settings(sync_enabled=True, mongo_uri="") diff --git a/tests/test_uburnode_proto_import.py b/tests/test_uburnode_proto_import.py new file mode 100644 index 0000000..285108b --- /dev/null +++ b/tests/test_uburnode_proto_import.py @@ -0,0 +1,72 @@ +"""proto gen 产物可 import。""" + +from google.protobuf.struct_pb2 import Value + +from app.uburnode_grpc.grpc_gen import ( + uburnode_pb2, + uburnode_pb2_grpc, + uburnode_somni_pb2, + uburnode_somni_pb2_grpc, +) + + +def test_handboard_package() -> None: + assert uburnode_pb2.DESCRIPTOR.package == "uburnode.v1" + assert hasattr(uburnode_pb2_grpc, "AudioServiceServicer") + assert hasattr(uburnode_pb2_grpc, "QuizServiceServicer") + + +def test_somni_package() -> None: + assert uburnode_somni_pb2.DESCRIPTOR.package == "uburnode.somni.v1" + assert hasattr(uburnode_somni_pb2_grpc, "QuizServiceServicer") + assert hasattr(uburnode_somni_pb2_grpc, "ReportServiceServicer") + assert hasattr(uburnode_somni_pb2_grpc, "AudioServiceServicer") + quiz = uburnode_somni_pb2.DESCRIPTOR.services_by_name["QuizService"] + assert quiz.full_name == "uburnode.somni.v1.QuizService" + assert "AnswerItem" not in uburnode_somni_pb2.DESCRIPTOR.message_types_by_name + answers_field = uburnode_somni_pb2.GetAnswerRes.DESCRIPTOR.fields_by_name["answers"] + assert answers_field.type == answers_field.TYPE_STRING + assert not answers_field.is_repeated + + +def test_get_hot_res_has_items() -> None: + from app.uburnode_grpc.grpc_gen import uburnode_somni_pb2 + + res = uburnode_somni_pb2.GetHotRes( + items=[uburnode_somni_pb2.HotKeyword(keyword="雨声", score=3)] + ) + assert res.items[0].keyword == "雨声" + assert res.items[0].score == 3 + + +def test_tag_dict_item_has_id_parent_status() -> None: + from app.uburnode_grpc.grpc_gen import uburnode_somni_pb2 + + item = uburnode_somni_pb2.TagDictItem( + id="t1", + parent_tag_id=Value(null_value=0), + status="启用", + type="content_form", + code="rain", + name="雨声", + name_en="Rain", + ) + assert item.id == "t1" + assert item.parent_tag_id.WhichOneof("kind") == "null_value" + assert item.status == "启用" + + +def test_audio_list_item_keeps_default_field_presence() -> None: + item = uburnode_somni_pb2.AudioListItem( + id="m1", + audio_name="雨声", + audio_url="", + cover_url="", + description="", + vip=0, + ) + assert item.HasField("audio_url") + assert item.HasField("cover_url") + assert item.HasField("description") + assert item.HasField("vip") + assert item.vip == 0 diff --git a/uv.lock b/uv.lock index 2db0795..0f95076 100644 --- a/uv.lock +++ b/uv.lock @@ -441,6 +441,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/e8/127dc2b246096ad50ef7c8d9b7b31d757787aeb796368bcdd4454e4204c4/grpcio-1.81.0-cp314-cp314-win_amd64.whl", hash = "sha256:b93cee313cae4e113fbb3a0ce1ea5633db6f63cfde2b2dc1d817429026b2a50b", size = 5070848, upload-time = "2026-06-01T05:56:19.735Z" }, ] +[[package]] +name = "grpcio-reflection" +version = "1.81.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/98/39a6972bb9a90750e32daabacaab7b4418e4384f7f2f686ff5af3d69094b/grpcio_reflection-1.81.0.tar.gz", hash = "sha256:5191db7aa6cab1b6981b0879fa44fdcdd43ba644f0301c40b976f813eb4eff06", size = 19192, upload-time = "2026-06-01T06:00:33.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/61/e472357ff5484f67c802568e70b3182df3d8eb1d0c2de38199d9a9a28bb2/grpcio_reflection-1.81.0-py3-none-any.whl", hash = "sha256:85322a9c1ab62d9823b1262a9d78d653b1710b99b5764cdcef2673cfe352b9c1", size = 22907, upload-time = "2026-06-01T06:00:16.714Z" }, +] + [[package]] name = "grpcio-tools" version = "1.81.0" @@ -2131,6 +2144,7 @@ dependencies = [ { name = "elasticsearch" }, { name = "fastapi" }, { name = "grpcio" }, + { name = "grpcio-reflection" }, { name = "loguru" }, { name = "motor" }, { name = "numpy" }, @@ -2161,6 +2175,7 @@ requires-dist = [ { name = "elasticsearch", specifier = ">=8.15.0,<9" }, { name = "fastapi", specifier = ">=0.115.0,<0.116" }, { name = "grpcio", specifier = ">=1.68.0" }, + { name = "grpcio-reflection", specifier = ">=1.68.0" }, { name = "grpcio-tools", marker = "extra == 'dev'", specifier = ">=1.68.0" }, { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.28.0" }, { name = "loguru", specifier = ">=0.7.0" },