Ship native iOS app, Wiki/TTS backend, and skeleton loading UX.

Replace the WebView shell with SwiftUI screens, add account-scoped Wiki and TTS APIs with adaptive review and photo scan support, and keep web/iOS pages usable while data loads asynchronously.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-08 15:13:59 +08:00
parent db5465c646
commit 05e173b293
115 changed files with 12709 additions and 1212 deletions
+115
View File
@@ -0,0 +1,115 @@
import base64
import json
import os
import tempfile
import unittest
from unittest.mock import MagicMock, patch
from services.tts_service import TtsService, TtsServiceError, tts_service
class _FakeResponse:
def __init__(self, body: bytes, content_type: str = "application/json"):
self._body = body
self.headers = MagicMock()
self.headers.get_content_type.return_value = content_type
def read(self):
return self._body
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
class TtsServiceTest(unittest.TestCase):
def tearDown(self):
get_settings = __import__("config", fromlist=["get_settings"]).get_settings
get_settings.cache_clear()
def test_not_configured_when_disabled(self):
with patch.dict(
os.environ,
{"TKMIND_TTS_ENABLED": "false", "TKMIND_TTS_BASE_URL": "http://127.0.0.1:19200"},
clear=False,
):
__import__("config", fromlist=["get_settings"]).get_settings.cache_clear()
self.assertFalse(tts_service.is_configured())
@patch("services.tts_service._open_tts_request")
def test_request_includes_user_agent(self, urlopen_mock: MagicMock):
wav = b"RIFFxxxx"
payload = {"code": 0, "data": base64.b64encode(wav).decode("ascii")}
urlopen_mock.return_value = _FakeResponse(json.dumps(payload).encode("utf-8"))
with patch.dict(
os.environ,
{"TKMIND_TTS_BASE_URL": "https://asr.tkmind.cn", "TKMIND_TTS_ENABLED": "true"},
clear=False,
):
__import__("config", fromlist=["get_settings"]).get_settings.cache_clear()
with tempfile.TemporaryDirectory() as tmp:
with patch("services.tts_service._CACHE_DIR", __import__("pathlib").Path(tmp)):
tts_service.synthesize("apple")
request = urlopen_mock.call_args.args[0]
self.assertEqual(request.get_header("User-agent"), "WordLoop-TTS/1.0")
@patch("services.tts_service._open_tts_request")
def test_synthesize_parses_json_base64(self, urlopen_mock: MagicMock):
wav = b"RIFFxxxx"
payload = {"code": 0, "message": "ok", "data": base64.b64encode(wav).decode("ascii")}
urlopen_mock.return_value = _FakeResponse(json.dumps(payload).encode("utf-8"))
with patch.dict(
os.environ,
{"TKMIND_TTS_BASE_URL": "http://127.0.0.1:19200", "TKMIND_TTS_ENABLED": "true"},
clear=False,
):
__import__("config", fromlist=["get_settings"]).get_settings.cache_clear()
with tempfile.TemporaryDirectory() as tmp:
with patch("services.tts_service._CACHE_DIR", __import__("pathlib").Path(tmp)):
audio, content_type = tts_service.synthesize("apple")
self.assertEqual(audio, wav)
self.assertEqual(content_type, "audio/wav")
@patch("services.tts_service._open_tts_request")
def test_synthesize_uses_cache_on_second_call(self, urlopen_mock: MagicMock):
wav = b"RIFFcached"
payload = {"code": 0, "data": base64.b64encode(wav).decode("ascii")}
urlopen_mock.return_value = _FakeResponse(json.dumps(payload).encode("utf-8"))
with patch.dict(
os.environ,
{"TKMIND_TTS_BASE_URL": "http://127.0.0.1:19200", "TKMIND_TTS_ENABLED": "true"},
clear=False,
):
__import__("config", fromlist=["get_settings"]).get_settings.cache_clear()
with tempfile.TemporaryDirectory() as tmp:
with patch("services.tts_service._CACHE_DIR", __import__("pathlib").Path(tmp)):
first, _ = tts_service.synthesize("apple")
second, _ = tts_service.synthesize("apple")
self.assertEqual(first, second)
self.assertEqual(urlopen_mock.call_count, 1)
@patch("services.tts_service._open_tts_request")
def test_upstream_error_message(self, urlopen_mock: MagicMock):
payload = {"code": 500, "message": "TTS 未启用(ENABLE_TTS=false", "data": None}
urlopen_mock.return_value = _FakeResponse(json.dumps(payload).encode("utf-8"))
with patch.dict(
os.environ,
{"TKMIND_TTS_BASE_URL": "http://127.0.0.1:19200", "TKMIND_TTS_ENABLED": "true"},
clear=False,
):
__import__("config", fromlist=["get_settings"]).get_settings.cache_clear()
with tempfile.TemporaryDirectory() as tmp:
with patch("services.tts_service._CACHE_DIR", __import__("pathlib").Path(tmp)):
with self.assertRaises(TtsServiceError) as ctx:
tts_service.synthesize("apple")
self.assertIn("TTS 未启用", str(ctx.exception))
def test_extract_audio_base64_from_dict(self):
value = TtsService._extract_audio_base64({"audio": "abc"})
self.assertEqual(value, "abc")