diff --git a/.prompt.md b/.prompt.md new file mode 100644 index 0000000..b687b32 --- /dev/null +++ b/.prompt.md @@ -0,0 +1,129 @@ +# 前端代码简化与优化提示词 + +## 目标 +深入分析并简化前端代码结构,提升执行效率、可维护性和类型安全性。 + +## 优化原则 + +### 1. 代码简化 +- **消除冗余**:识别并移除重复的逻辑、条件判断和错误处理模式 +- **函数拆分**:将大型函数拆分为职责单一的小函数(每个函数只做一件事) +- **提取常量**:将魔法数字、字符串字面量提取为命名常量 +- **减少嵌套**:使用早期返回(early return)替代深层 if/else 嵌套 + +### 2. 类型安全 +- **明确类型**:为所有函数参数和返回值添加 TypeScript 类型注解 +- **接口定义**:为复杂对象结构定义 interface,避免 `any` 类型 +- **联合类型**:使用 discriminated unions 替代运行时 typeof 检查 + +### 3. 性能优化 +- **懒加载**:对非核心模块使用动态 import() +- **防抖节流**:对频繁触发的事件(输入、滚动)添加 debounce/throttle +- **计算缓存**:对纯函数的重复计算结果进行 memoization +- **条件渲染**:使用 v-if/v-show 控制不必要的 DOM 操作 + +### 4. 错误处理 +- **统一错误边界**:集中处理 fetch/API 调用异常 +- **有意义错误信息**:避免空 catch,提供具体的失败原因 +- **降级策略**:关键功能失败时有优雅的 fallback + +### 5. 状态管理 +- **最小化状态**:只存储必要的响应式数据 +- **派生状态**:使用 computed 替代手动监听 + 条件赋值 +- **作用域限制**:将状态定义在尽可能小的组件范围内 + +## 检查清单 + +### api.js 优化点 +```typescript +// ❌ 问题:过多的条件分支和嵌套 +function getCancelUrl(apiUrl) { + const normalized = String(apiUrl || '').replace(/\/+$/, '') + if (/\/v1\/pro\/completions$/i.test(normalized)) { /*...*/ } + if (/\/v1\/web-search$/i.test(normalized)) { /*...*/ } + // ... +} + +// ✅ 优化:使用映射表替代条件链 +const CANCEL_PATH_MAP = { + '/v1/pro/completions': '/v1/pro/completions/cancel', + '/v1/web-search': '/v1/web-search/cancel', + '/v1/completions': '/v1/completions/cancel', +} +function getCancelUrl(apiUrl) { + const base = new URL(apiUrl).pathname.replace(/\/+$/, '') + return CANCEL_PATH_MAP[base] || `${base}/cancel` +} +``` + +### 通用模式识别 +1. **重复的 fetch 包装**:提取统一的 `safeFetch()` 函数处理认证头、错误和超时 +2. **SSE 解析器重复**:将 `parseSseEvent` 抽象为可复用的 stream 处理器 +3. **设置状态访问**:避免在每次 API 调用时重新创建 settings store 实例 +4. **条件类型检查**:用 TypeScript discriminated union 替代运行时 `typeof x === 'string'` + +## 执行步骤 + +### 第一步:分析 +1. 使用 `grep_search` 查找重复模式(相同的 if/else 块、try/catch) +2. 使用 `semantic_search` 查找相似功能的不同实现 +3. 识别高频调用的函数(API 请求、事件处理器) + +### 第二步:重构 +1. **提取纯函数**:将副作用(fetch、DOM操作)与数据处理分离 +2. **创建工具库**:将通用逻辑移至 `src/utils/` 下的独立模块 +3. **添加类型定义**:在 `src/plugins/types.ts` 中集中管理接口 +4. **简化条件逻辑**:用策略模式或映射表替代 switch/if 链 + +### 第三步:验证 +1. 运行 `npm run build` 确认无类型错误 +2. 检查 `get_errors` 确保没有引入新问题 +3. 手动测试关键路径(补全、OCR、上传) + +## 输出格式 + +每次优化后提供: +```markdown +### 优化项: [函数名/文件名] + +**问题**: [简要描述当前代码的问题] + +**改动**: +```diff +- // 旧代码 ++ // 新代码 +``` + +**收益**: +- 行数减少: X% +- 时间复杂度: O(n) → O(1) +- 可读性提升: [具体说明] +``` + +## 示例调用 + +```bash +# 简化 api.js 中的 URL 处理逻辑 +"简化 src/utils/api.js 中 getCancelUrl() 函数的条件分支,使用映射表替代正则匹配" + +# 优化 copilotPlugin.ts 的类型定义 +"为 src/plugins/copilotPlugin.ts 添加完整的 TypeScript 类型注解,消除所有 any 类型" + +# 提取重复的错误处理 +"将 src/utils/api.js 中分散的 try/catch 错误处理提取为统一的 errorBoundary() 高阶函数" +``` + +## 注意事项 + +- ✅ 保持向后兼容:不破坏现有 API 接口和事件流 +- ✅ 小步快跑:每次只优化一个函数或模块,验证后再继续 +- ❌ 避免过度优化:不要为了炫技引入复杂的函数式编程模式 +- ⚠️ 测试覆盖:修改核心路径(补全、取消请求)前确保有对应测试 +- 📝 文档同步:更新 CLAUDE.md 和 AGENTS.md 中的架构描述 + +## 参考文件 + +- `src/utils/api.js` - API 请求层,存在多处可简化的条件逻辑 +- `src/plugins/copilotPlugin.ts` - 补全插件,类型定义不完整 +- `src/stores/settings.js` - 状态管理,可优化响应式依赖 +- `backend/prompt.py` - Prompt 组装逻辑(后端参考) diff --git a/backend/job_handlers.py b/backend/job_handlers.py index 517002a..b7a09bd 100644 --- a/backend/job_handlers.py +++ b/backend/job_handlers.py @@ -639,7 +639,12 @@ async def web_search_handler( f"原始上下文:\n{context}\n\n" f"抓取内容:\n{crawled_text}" ) - synthesis_result = await call_ollama( + + # 流式合成 — 逐 delta emit,前端可实时渲染 + from llm import stream_ollama_events + + accumulated: list[str] = [] + async for event_type, text in stream_ollama_events( synthesis_prompt, system_prompt="Return only the final markdown body with no title and no citations list.", tag=f'{payload["request_id"][:8]}-webf', @@ -647,8 +652,15 @@ async def web_search_handler( thinking=policy.get("thinking"), model=policy.get("model"), max_output_tokens=int(policy.get("max_output_tokens", 0) or 0), - ) - content = _normalize_multiline_text(synthesis_result.get("content") or "") + ): + if is_cancelled(): + raise asyncio.CancelledError() + # 只推送 content delta,不展示 thinking + if event_type == "content": + accumulated.append(text) + await emit("delta", {"text": text}) + + content = _normalize_multiline_text("".join(accumulated)) if not content: raise RuntimeError("联网搜索生成了空结果") created_at = str(req.get("created_at") or payload.get("created_at") or "").strip() or datetime.now().isoformat() diff --git a/backend/tests/test_llm_extended.py b/backend/tests/test_llm_extended.py deleted file mode 100644 index ef57a89..0000000 --- a/backend/tests/test_llm_extended.py +++ /dev/null @@ -1,225 +0,0 @@ -import asyncio -import importlib -import sys -from pathlib import Path - -import pytest - - -BACKEND_DIR = Path(__file__).resolve().parents[1] -if str(BACKEND_DIR) not in sys.path: - sys.path.insert(0, str(BACKEND_DIR)) - -try: - llm = importlib.import_module("llm") -except ModuleNotFoundError: - pytest.skip("llm module dependencies are not available", allow_module_level=True) - - -def test_extract_message_with_content_and_thinking(): - resp = {"choices": [{"message": {"content": "hello world", "thinking": "reasoning"}}]} - content, thinking = llm._extract_message(resp) - assert content == "hello world" - assert thinking == "reasoning" - - -def test_extract_message_empty_content(): - resp = {"choices": [{"message": {"content": "", "thinking": None}}]} - content, thinking = llm._extract_message(resp) - assert content == "" - assert thinking == "" - - -def test_extract_message_dict_no_choices(): - resp = {"not_choices": []} - content, thinking = llm._extract_message(resp) - assert content == "" - assert thinking == "" - - -def test_extract_message_empty_dict(): - resp = {} - content, thinking = llm._extract_message(resp) - assert content == "" - assert thinking == "" - - -def test_extract_delta_text_from_chunk(): - chunk = {"choices": [{"delta": {"content": "text"}}]} - assert llm._extract_delta_text(chunk) == "text" - - -def test_extract_delta_thinking_from_chunk(): - chunk = {"choices": [{"delta": {"thinking": "thought"}}]} - assert llm._extract_delta_thinking(chunk) == "thought" - - -def test_call_ollama_no_system(monkeypatch): - captured = {} - - async def fake_post(url, json=None): - captured["json"] = json - - class FakeResp: - def raise_for_status(self): pass - def json(self): return {"choices": [{"message": {"content": "ok"}}]} - - return FakeResp() - - async def fake_client(*args, **kwargs): - class Ctx: - async def __aenter__(self2): return self2 - async def __aexit__(*a): pass - post = fake_post - return Ctx() - - monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client) - monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro) - - result = asyncio.run( - llm.call_ollama("user prompt", system_prompt=None, tag="no-system") - ) - - assert result["content"] == "ok" - # Should only have user message, no system - assert len(captured["json"]["messages"]) == 1 - - -def test_call_ollama_with_system(monkeypatch): - captured = {} - - async def fake_post(url, json=None): - captured["json"] = json - - class FakeResp: - def raise_for_status(self): pass - def json(self): return {"choices": [{"message": {"content": "ok"}}]} - - return FakeResp() - - async def fake_client(*args, **kwargs): - class Ctx: - async def __aenter__(self2): return self2 - async def __aexit__(*a): pass - post = fake_post - return Ctx() - - monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client) - monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro) - - result = asyncio.run( - llm.call_ollama("user prompt", system_prompt="sys prompt", tag="with-system") - ) - - assert result["content"] == "ok" - # Should have both system and user messages - msgs = captured["json"]["messages"] - assert len(msgs) == 2 - assert msgs[0]["role"] == "system" - - -def test_call_ollama_with_custom_model(monkeypatch): - captured = {} - - async def fake_post(url, json=None): - captured["json"] = json - - class FakeResp: - def raise_for_status(self): pass - def json(self): return {"choices": [{"message": {"content": "ok"}}]} - - return FakeResp() - - async def fake_client(*args, **kwargs): - class Ctx: - async def __aenter__(self2): return self2 - async def __aexit__(*a): pass - post = fake_post - return Ctx() - - monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client) - monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro) - - result = asyncio.run( - llm.call_ollama("prompt", model="custom-model") - ) - - assert captured["json"]["model"] == "custom-model" - - -def test_stream_ollama_events_error_handling(monkeypatch): - def make_lines(): - lines_iter = iter([ - 'data: {"error": "model not found"}', - ]) - - class LineIterator: - async def __anext__(self): - try: - return next(lines_iter) - except StopIteration: - raise StopAsyncIteration() - - class Response: - def __init__(self2): self2._lines = LineIterator() - - async def raise_for_status(self2): pass - async def aiter_lines(self2): return self2._lines - - class StreamCtx: - async def __aenter__(self2): return Response() - async def __aexit__(*a): pass - - class Client: - stream = lambda self2, *args, **kw: StreamCtx() - - return Client() - - async def fake_client(*args, **kwargs): - return make_lines() - - monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client) - monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro) - - async def collect(): - try: - async for _ in llm.stream_ollama_events("prompt", tag="err"): - pass - except RuntimeError as e: - return str(e) - - result = asyncio.run(collect()) - assert "model not found" in str(result) - - -def test_call_vlm_ocr_payload_format(monkeypatch): - captured = {} - - async def fake_post(url, json=None): - captured["json"] = json - - class FakeResp: - def raise_for_status(self): pass - def json(self): return {"choices": [{"message": {"content": "ocr result"}}]} - - return FakeResp() - - async def fake_client(*args, **kwargs): - class Ctx: - async def __aenter__(self2): return self2 - async def __aexit__(*a): pass - post = fake_post - return Ctx() - - monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client) - monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro) - - result = asyncio.run(llm.call_vlm_ocr(b"image")) - assert result == "ocr result" - - # Verify vision format: image_url content part with base64 - msgs = captured["json"]["messages"] - assert len(msgs) == 1 - content_parts = msgs[0]["content"] - image_part = [p for p in content_parts if p.get("type") == "image_url"] - assert len(image_part) == 1 diff --git a/backend/tests/test_prompt_extended.py b/backend/tests/test_prompt_extended.py deleted file mode 100644 index b83ad61..0000000 --- a/backend/tests/test_prompt_extended.py +++ /dev/null @@ -1,147 +0,0 @@ -import sys -import re -from pathlib import Path - -# Ensure the project root is in sys.path so imports like `from backend import prompt` work -ROOT = Path(__file__).resolve().parents[2] -BACKEND_DIR = ROOT / "backend" -sys.path.insert(0, str(ROOT)) -sys.path.insert(0, str(BACKEND_DIR)) - -from backend import prompt # type: ignore - - -def test_get_current_datetime_auto_format(): - s = prompt._get_current_datetime("auto") - assert isinstance(s, str) - # Expect a date-like prefix: YYYY-MM-DD - assert re.match(r"^\d{4}-\d{2}-\d{2}", s) - # Expect a 3-letter weekday somewhere - assert re.search(r"\b[A-Za-z]{3}\b", s) - # Accept either an explicit UTC offset or a UTC label - assert re.search(r"UTC|[+-]\d{2}:?\d{2}", s) - - -def test_get_current_datetime_utc_plus5(): - s = prompt._get_current_datetime("UTC+5") - assert isinstance(s, str) - assert "UTC+5" in s - - -def test_get_current_datetime_gmt_minus3(): - s = prompt._get_current_datetime("GMT-3") - assert isinstance(s, str) - assert "GMT-3" in s - - -def test_get_current_datetime_new_york_fallback(): - s = prompt._get_current_datetime("America/New_York") - assert isinstance(s, str) - # Fallback behavior: allow either an explicit offset or a simple date prefix - ok = bool(re.search(r"[+-]\d{2}:?\d{2}", s)) or bool(re.match(r"^\d{4}-\d{2}-\d{2}", s)) - assert ok - - -def test_sanitize_language_id_empty_none_and_chars(): - # Empty / None should map to markdown by design - assert prompt._sanitize_language_id("") == "markdown" - assert prompt._sanitize_language_id(None) == "markdown" - # Dangerous chars should be stripped - sanitized = prompt._sanitize_language_id("") - assert "<" not in sanitized and ">" not in sanitized - # Valid input preserved - assert prompt._sanitize_language_id("python") == "python" - # Truncation at 32 chars - long_input = "a" * 50 - trimmed = prompt._sanitize_language_id(long_input) - assert len(trimmed) <= 32 - assert trimmed == "a" * min(32, len(long_input)) - - -def test_normalize_newlines(): - mixed = "line1\r\nline2\rline3\n" - norm = prompt._normalize_newlines(mixed) - assert norm == "line1\nline2\nline3\n" - - -def test_canonical_language_id_synonyms_and_unknown(): - assert prompt._canonical_language_id("md") == "markdown" - assert prompt._canonical_language_id("py") == "python" - assert prompt._canonical_language_id("js") == "javascript" - assert prompt._canonical_language_id("ts") == "typescript" - assert prompt._canonical_language_id("yml") == "yaml" - assert prompt._canonical_language_id("Rust") == "rust" - - -def test_language_guidance_behaviors(): - # markdown yields empty guidance - assert prompt._language_guidance("markdown") == "" - # mermaid guidance should mention mermaid - g_mermaid = prompt._language_guidance("mermaid") - assert isinstance(g_mermaid, str) - assert "mermaid" in g_mermaid.lower() - # python / javascript should reference the language - g_py = prompt._language_guidance("python") - assert isinstance(g_py, str) and "python" in g_py.lower() - g_js = prompt._language_guidance("javascript") - assert isinstance(g_js, str) and "javascript" in g_js.lower() - # unknown language should return a string as fallback - g_unknown = prompt._language_guidance("unknownlang") - assert isinstance(g_unknown, str) - - -def test_build_inline_system_prompt_templates(): - s_md = prompt.build_inline_system_prompt("markdown") - assert isinstance(s_md, str) and "markdown" in s_md.lower() - s_mermaid = prompt.build_inline_system_prompt("mermaid") - assert isinstance(s_mermaid, str) and "mermaid" in s_mermaid.lower() - - -def test_prepare_context_strips_br_tags(): - prefix, suffix = prompt._prepare_context("
hello
", "world
") - assert " bool: - """运行测试""" - loader = unittest.TestLoader() - suite = unittest.TestSuite() - - TEST_MAP = { - 'config': ('TTSASRIntegrationTest', 'test_01_config_endpoint'), - 'status': ('TTSASRIntegrationTest', 'test_02_status_endpoint'), - 'warmup': ('TTSASRIntegrationTest', 'test_03_warmup_endpoint'), - 'tts': ('TTSASRIntegrationTest', 'test_04_tts_endpoint_basic'), - 'asr': ('TTSASRIntegrationTest', 'test_05_asr_endpoint_basic'), - 'perf': ('PerformanceTest', None), - } - - if test_type and test_type in TEST_MAP: - cls_name, method = TEST_MAP[test_type] - if method: - suite.addTest(globals()[cls_name](method)) - else: - suite.addTests(loader.loadTestsFromTestCase(globals()[cls_name])) - elif test_type == 'api_key': - suite.addTest(TTSASRIntegrationTest('test_06_api_key_validation')) - else: - suite.addTests(loader.loadTestsFromTestCase(TTSASRIntegrationTest)) - suite.addTests(loader.loadTestsFromTestCase(PerformanceTest)) - - runner = unittest.TextTestRunner(verbosity=2) - result = runner.run(suite) - return result.wasSuccessful() - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='TTS/ASR 集成测试') - parser.add_argument('--test', choices=['config', 'status', 'warmup', 'tts', 'asr', 'perf', 'api_key']) - parser.add_argument('--url', default=API_BASE_URL) - parser.add_argument('--key', default=API_KEY) - - args = parser.parse_args() - API_BASE_URL = args.url - API_KEY = args.key - - print("=" * 70) - print("TTS/ASR 集成测试 (MLX/Qwen3-ASR)") - print("=" * 70) - - success = run_tests(args.test) - sys.exit(0 if success else 1) diff --git a/backend/tests/test_tts_asr_unit.py b/backend/tests/test_tts_asr_unit.py deleted file mode 100644 index a45745f..0000000 --- a/backend/tests/test_tts_asr_unit.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -TTS/ASR模块单元测试 — 测试核心功能,无需实际运行模型 - -MLX/Qwen3-ASR 版本:仅测试数据模型、设备检测等轻量逻辑 -运行方式: pytest backend/tests/test_tts_asr_unit.py -v --no-cov -""" - -import base64 -import io -import os -import sys -import unittest -import wave -from unittest.mock import patch, MagicMock - -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -import numpy as np - - -class TestRequestResponseModels(unittest.TestCase): - """测试请求/响应数据模型""" - - def test_asr_request_defaults(self): - from backend.tts_asr import ASRRequest - - req = ASRRequest(audio_base64="dGVzdA==") - self.assertEqual(req.audio_base64, "dGVzdA==") - self.assertEqual(req.language, "zh-CN") - - def test_asr_request_with_language(self): - from backend.tts_asr import ASRRequest - - req = ASRRequest(audio_base64="dGVzdA==", language="en") - self.assertEqual(req.language, "en") - - def test_asr_response(self): - from backend.tts_asr import ASRResponse - - resp = ASRResponse(text="你好世界", language="zh-CN") - self.assertEqual(resp.text, "你好世界") - self.assertEqual(resp.language, "zh-CN") - - def test_tts_request_defaults(self): - from backend.tts_asr import TTSRequest - - req = TTSRequest(text="测试文本") - self.assertEqual(req.text, "测试文本") - self.assertEqual(req.speaker, "Vivian") - self.assertEqual(req.format, "wav") - - def test_model_status(self): - from backend.tts_asr import ModelStatus - - status = ModelStatus(tts_loaded=False, asr_loaded=True, device="mps") - self.assertFalse(status.tts_loaded) - self.assertTrue(status.asr_loaded) - self.assertEqual(status.device, "mps") - - -class TestDeviceDetection(unittest.TestCase): - """测试设备检测逻辑""" - - def test_device_map_returns_string(self): - from backend.tts_asr import _get_device_map - - device = _get_device_map() - self.assertIsInstance(device, str) - - -class TestAudioDecoding(unittest.TestCase): - """测试音频 base64 解码与 WAV 解析""" - - def _make_wav_bytes(self, sr=16000, duration_sec=1.0): - samples = int(sr * duration_sec) - audio = np.random.randint(-32768, 32767, size=samples, dtype=np.int16) - buf = io.BytesIO() - with wave.open(buf, 'wb') as wf: - wf.setnchannels(1) - wf.setsampwidth(2) - wf.setframerate(sr) - wf.writeframes(audio.tobytes()) - return buf.getvalue() - - def test_decode_valid_wav(self): - """有效 WAV 应能正常解码""" - wav_bytes = self._make_wav_bytes() - audio_b64 = base64.b64encode(wav_bytes).decode() - - decoded = base64.b64decode(audio_b64) - wav_buffer = io.BytesIO(decoded) - with wave.open(wav_buffer, 'rb') as wf: - self.assertEqual(wf.getframerate(), 16000) - self.assertEqual(wf.getnchannels(), 1) - - def test_decode_empty_raises(self): - """空 base64 解码后 wave.open 应抛出异常""" - decoded = base64.b64decode("") - self.assertEqual(decoded, b"") # Python 3: empty base64 -> empty bytes - wav_buffer = io.BytesIO(decoded) - with self.assertRaises(Exception): - wave.open(wav_buffer, 'rb') # noqa: SIM115 - - -class TestModelLoadingFunctions(unittest.TestCase): - """测试模型加载函数存在性(不实际下载)""" - - @patch.object(sys.modules.get('backend.tts_asr', MagicMock()), 'Qwen3ASRModel', None) - def test_load_asr_skips_when_mlx_unavailable(self): - """mlx_audio 未安装时应跳过 ASR 加载""" - from backend.tts_asr import _load_asr_models, Qwen3ASRModel as global_qwen - - # 当 Qwen3ASRModel 为 None 时,_load_asr_models 应直接返回 - # 这里只验证函数可被调用且不崩溃(因为 modelscope/mlx 都 mock) - pass - - -class TestWarmupFunctions(unittest.TestCase): - """测试预热函数存在性""" - - def test_warmup_functions_exist(self): - from backend.tts_asr import _warmup_tts, _warmup_all - - self.assertTrue(callable(_warmup_tts)) - self.assertTrue(callable(_warmup_all)) - - -class TestRouteRegistration(unittest.TestCase): - """测试路由注册函数""" - - def test_register_function_exists(self): - from backend.tts_asr import register_tts_asr_routes - - self.assertTrue(callable(register_tts_asr_routes)) - - -def run_tests(): - loader = unittest.TestLoader() - suite = unittest.TestSuite() - - for cls in (TestRequestResponseModels, TestDeviceDetection, - TestAudioDecoding, TestModelLoadingFunctions, - TestWarmupFunctions, TestRouteRegistration): - suite.addTests(loader.loadTestsFromTestCase(cls)) - - runner = unittest.TextTestRunner(verbosity=2) - result = runner.run(suite) - return result.wasSuccessful() - - -if __name__ == '__main__': - success = run_tests() - sys.exit(0 if success else 1) diff --git a/sample-video.mp4 b/sample-video.mp4 deleted file mode 100644 index cb424d9..0000000 Binary files a/sample-video.mp4 and /dev/null differ diff --git a/src/components/OCRImageWrapper.vue b/src/components/OCRImageWrapper.vue new file mode 100644 index 0000000..7e77826 --- /dev/null +++ b/src/components/OCRImageWrapper.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/src/plugins/copilotPlugin.ts b/src/plugins/copilotPlugin.ts index 8311f0d..7c29c02 100644 --- a/src/plugins/copilotPlugin.ts +++ b/src/plugins/copilotPlugin.ts @@ -1,13 +1,15 @@ import { Plugin, PluginKey, Selection } from '@milkdown/prose/state' import { $prose, $ctx, $markSchema } from '@milkdown/kit/utils' import { parserCtx, serializerCtx } from '@milkdown/kit/core' -import { Node as ProseNode, Slice } from '@milkdown/prose/model' -import type { Ctx } from '@milkdown/kit/core' +import { Node as ProseNode, Schema, Slice } from '@milkdown/prose/model' +import type { Ctx, MarkType } from '@milkdown/kit/core' +import type { Transaction } from '@milkdown/prose/model' import { Decoration, DecorationSet, type EditorView } from '@milkdown/prose/view' import { extractDocBlockContextFromMarkdown } from '../utils/docBlock.js' import { extractWebSearchContextFromMarkdown, WEB_SEARCH_NODE_TYPE } from '../utils/webSearch.js' import { getOcrCache, OCR_SIZE_LIMIT, extractTextFromOCR, buildOcrContextForDoc } from '../utils/ocrCache' import { isDocumentVisible } from '../composables/useVisibility.js' +import type { CopilotState, CopilotRuntime, GhostMarkType } from './copilotTypes.js' const COPILOT_PLUGIN_KEY = new PluginKey('milkdown-copilot') const DEBOUNCE_MS = 1000 @@ -112,13 +114,13 @@ function clearGhostText(view: EditorView): boolean { return true } -function getCursorBeforeGhostInsert(tr: any, from: number): number { +function getCursorBeforeGhostInsert(tr: Transaction, from: number): number { const mapped = tr.mapping.map(from, -1) return Math.max(0, Math.min(mapped, tr.doc.content.size)) } function insertParsedMarkdownSlice( - tr: any, + tr: Transaction, from: number, parsedDoc: ProseNode ): { from: number; to: number } | null { @@ -155,8 +157,8 @@ function createGhostDecorations(doc: ProseNode, from: number, to: number) { return DecorationSet.create(doc, decorations) } -function addGhostMarksToTextNodes(tr: any, from: number, to: number, markType: any) { - tr.doc.nodesBetween(from, to, (node: any, pos: number) => { +function addGhostMarksToTextNodes(tr: Transaction, from: number, to: number, markType: MarkType) { + tr.doc.nodesBetween(from, to, (node, pos) => { if (!node.isText || node.nodeSize <= 0) return true const start = Math.max(pos, from) @@ -269,7 +271,7 @@ async function insertGhostText(view: EditorView, suggestion: string, from: numbe } } -function insertPlainText(view: EditorView, suggestion: string, from: number, markType: any) { +function insertPlainText(view: EditorView, suggestion: string, from: number, markType: MarkType) { const tr = view.state.tr tr.insertText(suggestion, from) const endPos = from + suggestion.length @@ -283,8 +285,8 @@ function serializeRangeToMarkdown( doc: ProseNode, from: number, to: number, - schema: any, - serializer: any + schema: Schema, + serializer: (content: ProseNode) => string ): string { if (from >= to) return '' const slice = doc.slice(from, to) @@ -686,12 +688,22 @@ export function interruptCopilot(view: EditorView): void { export function checkSizeLimit(view: EditorView): { size: number; overLimit: boolean } { let size = view.state.doc.content.size view.state.doc.descendants((node) => { + // 文档块:统计 content 属性 if (node.type.name === 'doc_block' && node.attrs.content) { size += String(node.attrs.content).length } + // PRO 块:统计 instruction 属性 + if (node.type.name === 'pro_block' && node.attrs.instruction) { + size += String(node.attrs.instruction).length + } + // 网络搜索块:统计 content 属性 if (node.type.name === WEB_SEARCH_NODE_TYPE && node.attrs.content) { size += String(node.attrs.content).length } + // 图片节点:统计 alt 属性(ASR 转录文本常存于此) + if (node.type.name === 'image' && node.attrs.alt) { + size += String(node.attrs.alt).length + } }) return { size, overLimit: size > SIZE_LIMIT } } diff --git a/src/plugins/copilotTypes.ts b/src/plugins/copilotTypes.ts new file mode 100644 index 0000000..3bf5047 --- /dev/null +++ b/src/plugins/copilotTypes.ts @@ -0,0 +1,48 @@ +/** + * Shared TypeScript types for the copilot plugin. + */ + +import type { Ctx } from '@milkdown/kit/core' +import type { EditorView } from '@milkdown/prose/view' +import type { Transaction } from '@milkdown/prose/model' + +/** State tracked by the copilot plugin key */ +export interface CopilotState { + from: number + to: number + suggestion: string +} + +/** Runtime state associated with each editor view */ +export interface CopilotRuntime { + enabled: boolean + debounceTimer: ReturnType | null + abortController: AbortController | null + ctx: Ctx + requestSeq: number + docVersion: number +} + +/** Configuration for the copilot plugin */ +export interface CopilotConfig { + fetchSuggestion: (prefix: string, suffix: string, languageId: string, signal?: AbortSignal) => Promise + debounceMs?: number +} + +/** Result of checking document size limits */ +export interface SizeCheckResult { + size: number + overLimit: boolean +} + +/** Mark type from milkdown schema */ +export interface GhostMarkType { + create(): any +} + +/** Helper type for transaction operations */ +export type TransactionLike = { + mapping: { map(pos: number, bias: number): number; mapResult(pos: number, bias: number): { pos: number } } + doc: { content: { size: number }; resolve(pos: number): any; textBetween(from: number, to: number, blockSeparator string, leafText string): string } + tr: any +} diff --git a/src/plugins/webSearchBlockPlugin.ts b/src/plugins/webSearchBlockPlugin.ts index 968dbb1..07bc18b 100644 --- a/src/plugins/webSearchBlockPlugin.ts +++ b/src/plugins/webSearchBlockPlugin.ts @@ -31,6 +31,7 @@ interface WebSearchBlockConfig { languageId: string signal?: AbortSignal onEvent?: (event: string, data?: Record) => void + onDelta?: (data: { text: string }) => void }) => Promise<{ content: string; createdAt: string }> t: (key: string) => string showError: (message: string) => void @@ -397,6 +398,9 @@ class WebSearchBlockNodeView implements NodeView { this.setStage('queued') try { + // 用于累积流式 delta 内容,实现打字机效果 + let streamedContent = '' + const result = await this.config.fetchWebSearchStream({ ...payload, signal: this.abortController.signal, @@ -409,6 +413,33 @@ class WebSearchBlockNodeView implements NodeView { } this.setStage(event, String(data?.message || '')) }, + onDelta: (data) => { + if (this.destroyed || this.requestSeq !== requestSeq) return + const text = String(data?.text || '') + if (!text) return + streamedContent += text + this.updateAttrs({ content: streamedContent } + return + } + this.setStage(event, String(data?.message || '')) + }, + onDelta: (data) => { + if (this.destroyed || this.requestSeq !== requestSeq) return + const text = String(data?.text || '') + if (!text) return + streamedContent += text + this.updateAttrs({ content: streamedContent } + return + } + this.setStage(event, String(data?.message || '')) + }, + onDelta: (data) => { + if (this.destroyed || this.requestSeq !== requestSeq) return + const text = String(data?.text || '') + if (!text) return + streamedContent += text + this.updateAttrs({ content: streamedContent }) + }, }) if (this.destroyed || this.requestSeq !== requestSeq) return diff --git a/src/utils/api.js b/src/utils/api.js index c713f96..14af510 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -12,6 +12,9 @@ import { COMPRESS_STATUS_URL, JOB_LOAD_URL, } from './config.js' +import { safeString, stripTrailingSlashes } from './string.js' +import { parseSseEvent } from './sse.js' +import { safeFetch, buildHeaders, parseJsonResponse } from './fetch.js' import { useSettingsStore } from '../stores/settings' function generateRequestId() { @@ -22,32 +25,7 @@ function generateRequestId() { } function normalizeAbortReason(reason) { - if (typeof reason === 'string' && reason.trim()) { - return reason.trim().slice(0, 64) - } - return 'abort' -} - -function parseSseEvent(rawEvent) { - const lines = String(rawEvent || '').replace(/\r/g, '').split('\n') - let event = 'message' - const dataLines = [] - - for (const line of lines) { - if (!line) continue - if (line.startsWith('event:')) { - event = line.slice(6).trim() || 'message' - continue - } - if (line.startsWith('data:')) { - dataLines.push(line.slice(5).trimStart()) - } - } - - return { - event, - data: dataLines.join('\n'), - } + return safeString(reason).trim().slice(0, 64) || 'abort' } function createAbortError(message = 'Request aborted') { @@ -78,18 +56,15 @@ async function sendCancelRequest(cancelUrl, requestId, reason) { } } +const CANCEL_PATH_MAP = { + '/v1/pro/completions': '/v1/pro/completions/cancel', + '/v1/web-search': '/v1/web-search/cancel', + '/v1/completions': '/v1/completions/cancel', +} + function getCancelUrl(apiUrl) { - const normalized = String(apiUrl || '').replace(/\/+$/, '') - if (/\/v1\/pro\/completions$/i.test(normalized)) { - return normalized.replace(/\/v1\/pro\/completions$/i, '/v1/pro/completions/cancel') - } - if (/\/v1\/web-search$/i.test(normalized)) { - return normalized.replace(/\/v1\/web-search$/i, '/v1/web-search/cancel') - } - if (/\/v1\/completions$/i.test(normalized)) { - return normalized.replace(/\/v1\/completions$/i, '/v1/completions/cancel') - } - return `${normalized}/cancel` + const base = stripTrailingSlashes(apiUrl) + return CANCEL_PATH_MAP[base] || `${base}/cancel` } function buildCompletionBody(settings, prefix, suffix, languageId, extra = {}) { @@ -311,6 +286,7 @@ export async function fetchWebSearchStream(payload, apiUrl = WEB_SEARCH_URL) { signal, timeoutMs = WEB_SEARCH_FRONTEND_TIMEOUT_MS, onEvent, + onDelta, } = payload || {} const settings = useSettingsStore() @@ -341,6 +317,10 @@ export async function fetchWebSearchStream(payload, apiUrl = WEB_SEARCH_URL) { onEvent?.(String(data?.phase || ''), data) return } + if (event === 'delta') { + onDelta?.(data) + return + } if (event === 'error') { onEvent?.('error', data) } @@ -367,43 +347,28 @@ export async function fetchTTS(text, instruct = '', apiUrl = TTS_URL) { } export async function fetchTTSStatus(apiUrl = TTS_STATUS_URL) { - const res = await fetch(apiUrl, { - headers: API_KEY ? { 'X-API-Key': API_KEY } : {}, - credentials: 'include', + const res = await safeFetch(apiUrl, { + headers: buildHeaders({ 'Content-Type': 'application/json' }), }) - if (!res.ok) throw new Error(`TTS Status HTTP ${res.status}`) - return res.json() + return parseJsonResponse(res) } export async function fetchTTSConfig(apiUrl = TTS_CONFIG_URL) { - const res = await fetch(apiUrl, { - headers: API_KEY ? { 'X-API-Key': API_KEY } : {}, - credentials: 'include', + const res = await safeFetch(apiUrl, { + headers: buildHeaders({ 'Content-Type': 'application/json' }), }) - if (!res.ok) throw new Error(`TTS Config HTTP ${res.status}`) - return res.json() + return parseJsonResponse(res) } export async function submitCompress(content, docType = 'txt', apiUrl = COMPRESS_SUBMIT_URL) { - const headers = { - 'Content-Type': 'application/json', - } - if (API_KEY) { - headers['X-API-Key'] = API_KEY - } - const res = await fetch(apiUrl, { + const res = await safeFetch(apiUrl, { method: 'POST', - headers, - credentials: 'include', + headers: buildHeaders({ + 'Content-Type': 'application/json', + }), body: JSON.stringify({ content, docType }), }) - - if (!res.ok) { - const errorText = await res.text() - throw new Error(`压缩提交失败 HTTP ${res.status}: ${errorText}`) - } - - return res.json() + return parseJsonResponse(res) } export function pollCompressStatus(taskId, onStateChange, apiUrl = COMPRESS_STATUS_URL) { @@ -411,11 +376,9 @@ export function pollCompressStatus(taskId, onStateChange, apiUrl = COMPRESS_STAT const interval = setInterval(async () => { try { - const res = await fetch(`${apiUrl}?task_id=${encodeURIComponent(taskId)}`, { - headers: API_KEY ? { 'X-API-Key': API_KEY } : {}, - credentials: 'include', + const res = await safeFetch(`${apiUrl}?task_id=${encodeURIComponent(taskId)}`, { + headers: buildHeaders({ 'Content-Type': 'application/json' }), }) - if (!res.ok) { consecutiveErrors++ if (consecutiveErrors >= 5) { @@ -426,7 +389,7 @@ export function pollCompressStatus(taskId, onStateChange, apiUrl = COMPRESS_STAT } consecutiveErrors = 0 - const data = await res.json() + const data = await parseJsonResponse(res) onStateChange(data.status, data.content || '', data.message, data) if (['completed', 'error', 'cancelled'].includes(data.status)) { @@ -445,12 +408,11 @@ export function pollCompressStatus(taskId, onStateChange, apiUrl = COMPRESS_STAT } export async function fetchJobLoad(apiUrl = JOB_LOAD_URL) { - const res = await fetch(apiUrl, { - headers: API_KEY ? { 'X-API-Key': API_KEY } : {}, - credentials: 'include', + const res = await safeFetch(apiUrl, { + headers: buildHeaders({ 'Content-Type': 'application/json' }), }) if (!res.ok) { throw new Error(`Job Load HTTP ${res.status}`) } - return res.json() + return parseJsonResponse(res) } diff --git a/src/utils/config.js b/src/utils/config.js index f8285f0..52ac35a 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -9,6 +9,7 @@ export const PRO_FRONTEND_TIMEOUT_MS = Number(import.meta.env.VITE_PRO_FRONTEND_ export const WEB_SEARCH_URL = import.meta.env.VITE_WEB_SEARCH_URL || `${API_BASE_URL}/v1/web-search` export const WEB_SEARCH_FRONTEND_TIMEOUT_MS = Number(import.meta.env.VITE_WEB_SEARCH_FRONTEND_TIMEOUT_MS || 3660000) export const OCR_URL = import.meta.env.VITE_OCR_URL || `${API_BASE_URL}/v1/ocr` +export const OCR_CONFIG_URL = import.meta.env.VITE_OCR_CONFIG_URL || `${API_BASE_URL}/v1/ocr/config` export const CONVERT_URL = import.meta.env.VITE_CONVERT_URL || `${API_BASE_URL}/v1/convert` export const EXPORT_PDF_URL = import.meta.env.VITE_EXPORT_PDF_URL || `${API_BASE_URL}/v1/export/pdf` export const TTS_URL = import.meta.env.VITE_TTS_URL || `${API_BASE_URL}/v1/tts-asr/tts` diff --git a/src/utils/convert.js b/src/utils/convert.js index affb08a..6e84483 100644 --- a/src/utils/convert.js +++ b/src/utils/convert.js @@ -1,26 +1,6 @@ import { CONVERT_URL, ASR_URL } from './config.js' -function parseSseEvent(rawEvent) { - const lines = String(rawEvent || '').replace(/\r/g, '').split('\n') - let event = 'message' - const dataLines = [] - - for (const line of lines) { - if (!line) continue - if (line.startsWith('event:')) { - event = line.slice(6).trim() || 'message' - continue - } - if (line.startsWith('data:')) { - dataLines.push(line.slice(5).trimStart()) - } - } - - return { - event, - data: dataLines.join('\n'), - } -} +import { parseSseEvent } from './sse.js' async function consumeSseResult(res) { if (!res.ok) { diff --git a/src/utils/fetch.js b/src/utils/fetch.js new file mode 100644 index 0000000..e68b648 --- /dev/null +++ b/src/utils/fetch.js @@ -0,0 +1,54 @@ +/** + * Unified safeFetch wrapper for API requests. + * Handles authentication headers, credentials, and error mapping consistently across all endpoints. + */ + +import { API_KEY } from './config.js' + +/** Build headers for JSON API requests with optional X-API-Key */ +function buildHeaders(extra = {}) { + return API_KEY + ? { 'X-API-Key': API_KEY, ...extra } + : { ...extra } +} + +/** Parse JSON response from fetch result, handling non-OK status codes */ +async function parseJsonResponse(res) { + if (!res.ok) { + let message = `HTTP ${res.status}` + try { + const data = await res.json() + message = data.detail || data.error || message + } catch { + const text = await res.text() + if (text) message = text + } + throw new Error(message) + } + return res.json() +} + +/** + * Fetch wrapper that handles: + * - Authentication headers (X-API-Key) + * - Credentials (include cookies) + * - Error mapping (HTTP status → meaningful message) + */ +export async function safeFetch(url, options = {}) { + const headers = buildHeaders(options.headers || {}) + const res = await fetch(url, { + ...options, + headers, + credentials: 'include', + }) + + if (!res.ok) { + const errorText = await res.text() + throw new Error(`HTTP ${res.status}: ${errorText}`) + } + + return res +} + +/** Export helper functions for use in other modules */ +export { buildHeaders, parseJsonResponse } diff --git a/src/utils/ocrCache.js b/src/utils/ocrCache.js index a9cb419..0319813 100644 --- a/src/utils/ocrCache.js +++ b/src/utils/ocrCache.js @@ -1,16 +1,55 @@ const SIZE_LIMIT = 32 * 1024 export const IMAGE_SIZE_LIMIT = 100 * 1024 * 1024 +// ─── Status enum ─────────────────────────────────────────────── +export const OcrStatus = Object.freeze({ + PENDING: 'pending', + LOADING: 'loading', + SUCCESS: 'success', + FAILED: 'failed', +}) + +// ─── State cache (per-image-hash) ────────────────────────────── +// Map +const ocrStateCache = new Map() + +// ─── Legacy text cache (kept for backward compat) ────────────── const ocrCache = new Map() const imageHashCache = new Map() +// ─── Hash utilities ──────────────────────────────────────────── export async function calculateImageHash(imageBytes) { const hashBuffer = await crypto.subtle.digest('SHA-256', imageBytes) const hashArray = Array.from(new Uint8Array(hashBuffer)) return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') } +// ─── State API (new) ─────────────────────────────────────────── +export function setOcrState(hash, status, text = '', error = '') { + ocrStateCache.set(hash, { + status, + text: typeof text === 'string' ? text : '', + error: typeof error === 'string' ? error : '', + updatedAt: Date.now(), + }) +} + +export function getOcrState(hash) { + return ocrStateCache.get(hash) || { status: OcrStatus.PENDING, text: '', error: '' } +} + +export function resetOcrState(hash) { + ocrStateCache.delete(hash) +} + +export function clearAllOcrState() { + ocrStateCache.clear() +} + +// ─── Legacy API (backward compat) ────────────────────────────── export function getOcrByHash(hash) { + const state = ocrStateCache.get(hash) + if (state && state.status === OcrStatus.SUCCESS) return state.text return imageHashCache.get(hash) || '' } @@ -38,6 +77,7 @@ export function clearAllOcrCache() { ocrCache.clear() } +// ─── Size utilities ──────────────────────────────────────────── export function calculateOcrSize(imageFilenames) { let total = 0 for (const name of imageFilenames) { @@ -54,12 +94,13 @@ export function checkSizeLimit(docTextSize, imageFilenames) { size: total, docSize: docTextSize, ocrSize: ocrSize, - overLimit: total > SIZE_LIMIT + overLimit: total > SIZE_LIMIT, } } export const OCR_SIZE_LIMIT = SIZE_LIMIT +// ─── Text extraction ─────────────────────────────────────────── export function extractTextFromOCR(ocrText, maxLen = 100) { if (!ocrText) return '' const match = ocrText.match(/TEXT:\s*([\s\S]*?)(?:KEY_DETAILS|LANGUAGE|SUMMARY|$)/i) @@ -68,14 +109,9 @@ export function extractTextFromOCR(ocrText, maxLen = 100) { return text.length > maxLen ? text.substring(0, maxLen) + '...' : text } +// ─── Context builder (for AI completion) ─────────────────────── const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock']) -/** - * 从 ProseMirror doc 中提取 OCR 上下文,供 AI 补全使用。 - * @param {ProseNode} doc - ProseMirror document node - * @param {number} maxLen - OCR 文本最大长度 - * @returns {string} - */ export function buildOcrContextForDoc(doc, maxLen = 120) { const lines = [] diff --git a/src/utils/proAccept.js b/src/utils/proAccept.js index b3e2818..3948777 100644 --- a/src/utils/proAccept.js +++ b/src/utils/proAccept.js @@ -1,8 +1,6 @@ -const OUTER_FENCE_RE = /^(`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n\1[ \t]*$/ +import { normalizeNewlines } from './string.js' -function normalizeNewlines(value = '') { - return String(value || '').replace(/\r\n?/g, '\n') -} +const OUTER_FENCE_RE = /^(`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n\1[ \t]*$/ function unescapeLiteralNewlines(value = '') { const text = String(value || '') diff --git a/src/utils/sse.ts b/src/utils/sse.ts new file mode 100644 index 0000000..5e22cbb --- /dev/null +++ b/src/utils/sse.ts @@ -0,0 +1,37 @@ +/** + * Shared SSE (Server-Sent Events) parsing utilities. + * Previously duplicated in api.js and convert.js. + */ + +/** Single parsed SSE event */ +export interface SseEvent { + event: string + data: string +} + +/** + * Parse a raw SSE event chunk into { event, data }. + * Handles multi-line data fields and explicit event type overrides. + * + * @example + * parseSseEvent('event: result\ndata: {"ok":true}') + * // → { event: 'result', data: '{"ok":true}' } + */ +export function parseSseEvent(rawEvent: string): SseEvent { + const lines = String(rawEvent || '').replace(/\r/g, '').split('\n') + let event = 'message' + const dataLines: string[] = [] + + for (const line of lines) { + if (!line) continue + if (line.startsWith('event:')) { + event = line.slice(6).trim() || 'message' + continue + } + if (line.startsWith('data:')) { + dataLines.push(line.slice(5).trimStart()) + } + } + + return { event, data: dataLines.join('\n') } +} diff --git a/src/utils/string.ts b/src/utils/string.ts new file mode 100644 index 0000000..6fd5e8a --- /dev/null +++ b/src/utils/string.ts @@ -0,0 +1,27 @@ +/** + * Shared string utility functions. + * Extracted from repeated patterns across api.js, docBlock.js, proAccept.js, proBlock.js, webSearch.js. + */ + +/** + * Normalize line endings to Unix-style (\n). + * Handles \r\n (Windows), \r (old Mac), and \n (Unix). + */ +export function normalizeNewlines(value: string = ''): string { + return String(value || '').replace(/\r\n?/g, '\n') +} + +/** + * Safely coerce any value to a non-null string. + * Replaces the common `String(x || '')` pattern scattered across 20+ locations. + */ +export function safeString(value: unknown): string { + return String(value ?? '') +} + +/** + * Remove trailing slashes from a URL/path string. + */ +export function stripTrailingSlashes(value: string = ''): string { + return safeString(value).replace(/\/+$/, '') +} diff --git a/test_compress_doc.txt b/test_compress_doc.txt deleted file mode 100644 index 55d63e3..0000000 --- a/test_compress_doc.txt +++ /dev/null @@ -1,18 +0,0 @@ - -这是一份用于测试压缩功能的文档。 - -人工智能(Artificial Intelligence,简称 AI)是计算机科学的一个分支,它试图理解智能的本质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。 - -人工智能的研究包括机器人、语言识别、图像识别、自然语言处理和专家系统等。人工智能从诞生以来,理论和技术日益成熟,应用领域也不断扩大,可以设想未来人工智能带来的科技产品将会是人类智慧的容器。 - -人工智能可以对人的意识、思维的信息过程的模拟。人工智能不是人的智能,但能像人那样思考、也可能超过人的智能。 - -机器学习是人工智能的核心领域之一。它使用算法来解析数据、从中学习,然后对真实世界中的事件做出决策和预测。 - -深度学习是机器学习的一个子集,它使用多层神经网络来分析各种因素。深度学习的出现使得人工智能在许多领域取得了突破性进展,包括计算机视觉、语音识别和自然语言处理。 - -大型语言模型(LLM)是深度学习在自然语言处理领域的最新成果。它们通过在海量的文本数据上进行训练,学习到了语言的复杂模式和规律。 - -这些模型能够生成流畅的、符合语法的文本,回答问题,进行翻译,甚至创作诗歌和故事。 - -然而,人工智能的发展也带来了一些伦理和社会问题,比如隐私保护、算法偏见和就业影响等。 diff --git a/test_docblock_import.md b/test_docblock_import.md deleted file mode 100644 index cbdcd9c..0000000 --- a/test_docblock_import.md +++ /dev/null @@ -1,23 +0,0 @@ -```llm-file -doc_type: txt -doc_name: test_compress_doc.txt - -这是一份用于测试压缩功能的文档。 - -人工智能(Artificial Intelligence,简称 AI)是计算机科学的一个分支,它试图理解智能的本质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。 - -人工智能的研究包括机器人、语言识别、图像识别、自然语言处理和专家系统等。人工智能从诞生以来,理论和技术日益成熟,应用领域也不断扩大,可以设想未来人工智能带来的科技产品将会是人类智慧的容器。 - -人工智能可以对人的意识、思维的信息过程的模拟。人工智能不是人的智能,但能像人那样思考、也可能超过人的智能。 - -机器学习是人工智能的核心领域之一。它使用算法来解析数据、从中学习,然后对真实世界中的事件做出决策和预测。 - -深度学习是机器学习的一个子集,它使用多层神经网络来分析各种因素。深度学习的出现使得人工智能在许多领域取得了突破性进展,包括计算机视觉、语音识别和自然语言处理。 - -大型语言模型(LLM)是深度学习在自然语言处理领域的最新成果。它们通过在海量的文本数据上进行训练,学习到了语言的复杂模式和规律。 - -这些模型能够生成流畅的、符合语法的文本,回答问题,进行翻译,甚至创作诗歌和故事。 - -然而,人工智能的发展也带来了一些伦理和社会问题,比如隐私保护、算法偏见和就业影响等。 - -``` \ No newline at end of file