mirror of
https://github.com/This-weiyuqing/mi-fitness-health.git
synced 2026-10-18 11:13:48 +08:00
保存 NAS 健康系统 UI7 代码与本机维护基线
This commit is contained in:
1 parent
1767b4921a
commit
8546ba23ff
80 files changed
+4984
-381
No files matched your search
@@ -0,0 +1,98 @@
|
||||
"""受限只读 MCP:复用查询服务,数据库与凭据表双重隔离。"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
from contextlib import contextmanager
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from mi_fitness_mcp.services.query_service import QueryService
|
||||
from mi_fitness_mcp.storage import Database
|
||||
|
||||
TABLES = {'daily_activity', 'sleep_sessions', 'body_measurements', 'heart_rate_samples',
|
||||
'workouts', 'spo2_samples', 'stress_samples', 'abnormal_heart_beat_events'}
|
||||
METHODS = {'daily_activity': 'get_daily_summaries', 'sleep': 'get_sleep_sessions',
|
||||
'body_measurements': 'get_body_measurements', 'heart_rate': 'get_heart_rate_samples',
|
||||
'workouts': 'get_workouts', 'spo2': 'get_spo2_samples', 'stress': 'get_stress_samples',
|
||||
'abnormal_heart_beat': 'get_abnormal_heart_beat_events'}
|
||||
PRIVATE_FIELDS = {'user_id', 'device_id', 'id', 'source_record_id', 'sleep_id', 'workout_id', 'event_id'}
|
||||
|
||||
|
||||
class ReadOnlyDatabase(Database):
|
||||
def __init__(self, path):
|
||||
self.db_path = Path(path).resolve()
|
||||
|
||||
@contextmanager
|
||||
def _get_connection(self):
|
||||
conn = sqlite3.connect(self.db_path.as_uri() + '?mode=ro', uri=True)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute('PRAGMA query_only=ON')
|
||||
|
||||
def authorize(action, table, column, database, trigger):
|
||||
if action == sqlite3.SQLITE_READ:
|
||||
return sqlite3.SQLITE_OK if table in TABLES else sqlite3.SQLITE_DENY
|
||||
if action in (sqlite3.SQLITE_SELECT, sqlite3.SQLITE_FUNCTION, sqlite3.SQLITE_RECURSIVE):
|
||||
return sqlite3.SQLITE_OK
|
||||
return sqlite3.SQLITE_DENY
|
||||
|
||||
conn.set_authorizer(authorize)
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def strip_private(value):
|
||||
if isinstance(value, dict):
|
||||
return {k: strip_private(v) for k, v in value.items() if k not in PRIVATE_FIELDS}
|
||||
if isinstance(value, list):
|
||||
return [strip_private(v) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def query_records(path, kind, start_date, end_date):
|
||||
if kind not in METHODS:
|
||||
raise ValueError('不支持的健康数据类型')
|
||||
start, end = date.fromisoformat(start_date), date.fromisoformat(end_date)
|
||||
if not 0 <= (end - start).days < 366:
|
||||
raise ValueError('单次查询范围必须为 1 至 366 天')
|
||||
db = ReadOnlyDatabase(path)
|
||||
with db._get_connection() as conn:
|
||||
users = set()
|
||||
for table in sorted(TABLES):
|
||||
users.update(r[0] for r in conn.execute('SELECT DISTINCT user_id FROM ' + table))
|
||||
if len(users) != 1:
|
||||
raise ValueError('数据库中需要恰好一个健康数据账号')
|
||||
service = QueryService(db, next(iter(users)))
|
||||
records = getattr(service, METHODS[kind])(start.isoformat(), end.isoformat())
|
||||
if len(records) > 2000:
|
||||
raise ValueError('结果超过 2000 条,请缩小日期范围;未截断或省略数据')
|
||||
return strip_private(records)
|
||||
|
||||
|
||||
def main(transport='stdio'):
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.transport_security import TransportSecuritySettings
|
||||
|
||||
database = Path(os.environ.get('HEALTH_READONLY_DB', '/health/mi_fitness.db'))
|
||||
mcp = FastMCP('NAS Health Read Only', host='0.0.0.0', port=8323, log_level='WARNING',
|
||||
stateless_http=True, json_response=True,
|
||||
transport_security=TransportSecuritySettings(
|
||||
allowed_hosts=['localhost:*', '127.0.0.1:*', 'readonly:8323'],
|
||||
allowed_origins=[]))
|
||||
|
||||
@mcp.tool(annotations={'readOnlyHint': True, 'destructiveHint': False, 'openWorldHint': False})
|
||||
def get_health_records(kind: str, start_date: str, end_date: str) -> dict:
|
||||
"""查询 NAS 已保存的健康记录,不触发同步。日期为北京时间 YYYY-MM-DD。
|
||||
|
||||
kind: daily_activity、sleep、body_measurements、heart_rate、workouts、spo2、stress、abnormal_heart_beat。
|
||||
单次最多 366 天和 2000 条,超限请缩小范围。单位:步、米、千卡、分钟、kg、bpm、百分比。
|
||||
"""
|
||||
records = query_records(database, kind, start_date, end_date)
|
||||
return {'timezone': 'Asia/Shanghai', 'kind': kind, 'count': len(records), 'data': records}
|
||||
|
||||
mcp.run(transport=transport)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user