@researai/deepscientist 1.5.8 → 1.5.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +186 -21
- package/README.md +108 -95
- package/assets/branding/connector-qq.png +0 -0
- package/assets/branding/connector-rokid.png +0 -0
- package/assets/branding/connector-weixin.png +0 -0
- package/assets/branding/projects.png +0 -0
- package/bin/ds.js +172 -13
- package/docs/assets/branding/projects.png +0 -0
- package/docs/en/00_QUICK_START.md +308 -70
- package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
- package/docs/en/09_DOCTOR.md +41 -5
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
- package/docs/en/11_LICENSE_AND_RISK.md +256 -0
- package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +427 -0
- package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/en/README.md +79 -0
- package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
- package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
- package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
- package/docs/images/weixin/weixin-settings-bind.svg +57 -0
- package/docs/zh/00_QUICK_START.md +315 -74
- package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
- package/docs/zh/09_DOCTOR.md +41 -5
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
- package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
- package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +423 -0
- package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/zh/README.md +126 -0
- package/install.sh +0 -34
- package/package.json +3 -3
- package/pyproject.toml +2 -2
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/annotations.py +343 -0
- package/src/deepscientist/artifact/arxiv.py +484 -37
- package/src/deepscientist/artifact/metrics.py +1 -3
- package/src/deepscientist/artifact/service.py +1347 -111
- package/src/deepscientist/arxiv_library.py +275 -0
- package/src/deepscientist/bash_exec/service.py +9 -0
- package/src/deepscientist/bridges/builtins.py +2 -0
- package/src/deepscientist/bridges/connectors.py +447 -0
- package/src/deepscientist/channels/__init__.py +2 -0
- package/src/deepscientist/channels/builtins.py +3 -1
- package/src/deepscientist/channels/qq.py +1 -1
- package/src/deepscientist/channels/qq_gateway.py +1 -1
- package/src/deepscientist/channels/relay.py +7 -1
- package/src/deepscientist/channels/weixin.py +59 -0
- package/src/deepscientist/channels/weixin_ilink.py +317 -0
- package/src/deepscientist/config/models.py +22 -2
- package/src/deepscientist/config/service.py +431 -60
- package/src/deepscientist/connector/__init__.py +4 -0
- package/src/deepscientist/connector/connector_profiles.py +481 -0
- package/src/deepscientist/connector/lingzhu_support.py +668 -0
- package/src/deepscientist/connector/qq_profiles.py +206 -0
- package/src/deepscientist/connector/weixin_support.py +663 -0
- package/src/deepscientist/connector_profiles.py +1 -374
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +295 -5
- package/src/deepscientist/daemon/api/router.py +16 -1
- package/src/deepscientist/daemon/app.py +1130 -61
- package/src/deepscientist/doctor.py +5 -2
- package/src/deepscientist/gitops/diff.py +120 -29
- package/src/deepscientist/lingzhu_support.py +1 -182
- package/src/deepscientist/mcp/server.py +14 -5
- package/src/deepscientist/prompts/builder.py +29 -1
- package/src/deepscientist/qq_profiles.py +1 -196
- package/src/deepscientist/quest/node_traces.py +152 -2
- package/src/deepscientist/quest/service.py +169 -43
- package/src/deepscientist/quest/stage_views.py +172 -9
- package/src/deepscientist/registries/baseline.py +56 -4
- package/src/deepscientist/runners/codex.py +55 -3
- package/src/deepscientist/weixin_support.py +1 -0
- package/src/prompts/connectors/lingzhu.md +3 -1
- package/src/prompts/connectors/weixin.md +230 -0
- package/src/prompts/system.md +9 -0
- package/src/skills/idea/SKILL.md +16 -0
- package/src/skills/idea/references/literature-survey-template.md +24 -0
- package/src/skills/idea/references/related-work-playbook.md +4 -0
- package/src/skills/idea/references/selection-gate.md +9 -0
- package/src/skills/write/SKILL.md +1 -1
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-m2FNtwbn.js → AiManusChatView-D0mTXG4-.js} +156 -48
- package/src/ui/dist/assets/{AnalysisPlugin-BMTF8EGL.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-BEOWgxCI.js → CliPlugin-DrV8je02.js} +164 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-BCXvjqmb.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DaJcy3nD.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ByfeIq4K.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-Cksf3VZ-.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
- package/src/ui/dist/assets/{ImageViewerPlugin-CFz-OsTS.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-CJ1cJzoX.js → LabCopilotPanel-1qSow1es.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-BF3dVJwa.js → LabPlugin-eQpPPCEp.js} +2 -1
- package/src/ui/dist/assets/{LatexPlugin-DDkwZ6Sj.js → LatexPlugin-BwRfi89Z.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-HAuvurcT.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-BtoTYy2C.js → MarketplacePlugin-C2y_556i.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-CSJYx7b-.js → NotebookEditor-BRzJbGsn.js} +12 -12
- package/src/ui/dist/assets/{NotebookEditor-DQgRezm_.js → NotebookEditor-DIX7Mlzu.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DPa_-fv6.js → PdfLoader-DzRaTAlq.js} +14 -7
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BZpXOEjm.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
- package/src/ui/dist/assets/{PdfViewerPlugin-BT8a6wGR.js → PdfViewerPlugin-BwtICzue.js} +103 -34
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
- package/src/ui/dist/assets/{SearchPlugin-D_blveZi.js → SearchPlugin-DHeIAMsx.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-Btx0M3hX.js → TextViewerPlugin-C3tCmFox.js} +5 -4
- package/src/ui/dist/assets/{VNCViewer-DImJO4rO.js → VNCViewer-CQsKVm3t.js} +10 -10
- package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
- package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
- package/src/ui/dist/assets/{code-BUfXGJSl.js → code-XfbSR8K2.js} +1 -1
- package/src/ui/dist/assets/{file-content-VqamwI3X.js → file-content-BjxNaIfy.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-C_wOoS7a.js → file-diff-panel-D_lLVQk0.js} +1 -1
- package/src/ui/dist/assets/{file-socket-D2bTuMVP.js → file-socket-D9x_5vlY.js} +1 -1
- package/src/ui/dist/assets/{image-BZkGJ4mM.js → image-BhWT33W1.js} +1 -1
- package/src/ui/dist/assets/{index-DdRW6RMJ.js → index--c4iXtuy.js} +12 -12
- package/src/ui/dist/assets/{index-CxkvSeKw.js → index-BDxipwrC.js} +2 -2
- package/src/ui/dist/assets/{index-DjggJovS.js → index-DZTZ8mWP.js} +14934 -9613
- package/src/ui/dist/assets/{index-DXZ1daiJ.css → index-Dqj-Mjb4.css} +2 -13
- package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
- package/src/ui/dist/assets/{monaco-DHMc7kKM.js → monaco-K8izTGgo.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
- package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- package/src/ui/dist/assets/{popover-B85oCgCS.js → popover-yFK1J4fL.js} +1 -1
- package/src/ui/dist/assets/{project-sync-DOMCcPac.js → project-sync-PENr2zcz.js} +1 -74
- package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
- package/src/ui/dist/assets/{sigma-BO2rQrl3.js → sigma-DEuYJqTl.js} +1 -1
- package/src/ui/dist/assets/{index-D9QIGcmc.js → square-check-big-omoSUmcd.js} +2 -13
- package/src/ui/dist/assets/{trash-BsVEH_dV.js → trash--F119N47.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-b8L6JuZm.js → useCliAccess-D31UR23I.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-BY7uA9hV.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-BwyVuUIK.js → wrap-text-CZ613PM5.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-RDpLugQP.js → zoom-out-BgDLAv3z.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
- package/src/ui/dist/assets/AutoFigurePlugin-DxPdMUNb.js +0 -8149
- package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
- package/src/ui/dist/assets/Stepper-DH2k75Vo.js +0 -158
- package/src/ui/dist/assets/bibtex-B-Hqu0Sg.js +0 -189
- package/src/ui/dist/assets/file-utils--zJCPN1i.js +0 -109
- package/src/ui/dist/assets/message-square-FUIPIhU2.js +0 -16
- package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
- package/src/ui/dist/assets/tooltip-B1OspAkx.js +0 -108
package/pyproject.toml
CHANGED
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "deepscientist"
|
|
7
|
-
version = "1.5.
|
|
7
|
+
version = "1.5.11"
|
|
8
8
|
description = "DeepScientist Core skeleton"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
|
-
license = { text = "
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "OpenAI Codex" }
|
|
14
14
|
]
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import unquote
|
|
8
|
+
|
|
9
|
+
from .quest import QuestService
|
|
10
|
+
from .shared import ensure_dir, generate_id, read_json, utc_now, write_json
|
|
11
|
+
|
|
12
|
+
_QUEST_FILE_PREFIX = "quest-file::"
|
|
13
|
+
_SCHEMA_VERSION = 1
|
|
14
|
+
_DEFAULT_AUTHOR_COLOR = "#F1E9D0"
|
|
15
|
+
_ALLOWED_KINDS = {"note", "question", "task"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _json_clone(value: Any) -> Any:
|
|
19
|
+
return json.loads(json.dumps(value))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AnnotationService:
|
|
23
|
+
def __init__(self, home: Path) -> None:
|
|
24
|
+
self.home = home.resolve()
|
|
25
|
+
self._lock = threading.Lock()
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _parse_quest_file_id(file_id: str) -> tuple[str, str, str]:
|
|
29
|
+
raw = str(file_id or "").strip()
|
|
30
|
+
if not raw.startswith(_QUEST_FILE_PREFIX):
|
|
31
|
+
raise ValueError("Only quest file ids are supported for annotations.")
|
|
32
|
+
payload = raw[len(_QUEST_FILE_PREFIX) :]
|
|
33
|
+
project_id, encoded_document_id, encoded_path = (payload.split("::", 2) + ["", ""])[:3]
|
|
34
|
+
project_id = str(project_id or "").strip()
|
|
35
|
+
if not project_id or not encoded_document_id:
|
|
36
|
+
raise ValueError("Invalid quest file id.")
|
|
37
|
+
document_id = unquote(encoded_document_id)
|
|
38
|
+
relative_path = unquote(encoded_path or encoded_document_id)
|
|
39
|
+
if not document_id:
|
|
40
|
+
raise ValueError("Invalid quest file id.")
|
|
41
|
+
return project_id, document_id, relative_path
|
|
42
|
+
|
|
43
|
+
def _quest_root(self, project_id: str) -> Path:
|
|
44
|
+
quest_root = self.home / "quests" / project_id
|
|
45
|
+
if not quest_root.exists():
|
|
46
|
+
raise FileNotFoundError(f"Unknown quest `{project_id}`.")
|
|
47
|
+
return quest_root
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _manifest_path(quest_root: Path) -> Path:
|
|
51
|
+
return quest_root / ".ds" / "annotations" / "index.json"
|
|
52
|
+
|
|
53
|
+
def _load_manifest(self, quest_root: Path) -> dict[str, Any]:
|
|
54
|
+
payload = read_json(self._manifest_path(quest_root), default=None)
|
|
55
|
+
if not isinstance(payload, dict):
|
|
56
|
+
payload = {}
|
|
57
|
+
items = payload.get("items")
|
|
58
|
+
if not isinstance(items, list):
|
|
59
|
+
items = []
|
|
60
|
+
return {
|
|
61
|
+
"schema_version": _SCHEMA_VERSION,
|
|
62
|
+
"updated_at": str(payload.get("updated_at") or utc_now()),
|
|
63
|
+
"items": [dict(item) for item in items if isinstance(item, dict)],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def _save_manifest(self, quest_root: Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
67
|
+
normalized = {
|
|
68
|
+
"schema_version": _SCHEMA_VERSION,
|
|
69
|
+
"updated_at": utc_now(),
|
|
70
|
+
"items": [dict(item) for item in (payload.get("items") or []) if isinstance(item, dict)],
|
|
71
|
+
}
|
|
72
|
+
ensure_dir(self._manifest_path(quest_root).parent)
|
|
73
|
+
write_json(self._manifest_path(quest_root), normalized)
|
|
74
|
+
return normalized
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _normalize_kind(value: object) -> str:
|
|
78
|
+
kind = str(value or "note").strip().lower()
|
|
79
|
+
return kind if kind in _ALLOWED_KINDS else "note"
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _normalize_tags(value: object) -> list[str]:
|
|
83
|
+
if not isinstance(value, list):
|
|
84
|
+
return []
|
|
85
|
+
result: list[str] = []
|
|
86
|
+
seen: set[str] = set()
|
|
87
|
+
for raw in value:
|
|
88
|
+
tag = str(raw or "").strip()
|
|
89
|
+
if not tag or tag in seen:
|
|
90
|
+
continue
|
|
91
|
+
seen.add(tag)
|
|
92
|
+
result.append(tag)
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def _normalize_position(cls, value: object) -> dict[str, Any]:
|
|
97
|
+
payload = _json_clone(value if isinstance(value, dict) else {})
|
|
98
|
+
if not isinstance(payload, dict):
|
|
99
|
+
raise ValueError("`position` must be an object.")
|
|
100
|
+
page_number = payload.get("pageNumber")
|
|
101
|
+
if not isinstance(page_number, int) or page_number <= 0:
|
|
102
|
+
raise ValueError("`position.pageNumber` must be a positive integer.")
|
|
103
|
+
if not isinstance(payload.get("boundingRect"), dict):
|
|
104
|
+
raise ValueError("`position.boundingRect` is required.")
|
|
105
|
+
rects = payload.get("rects")
|
|
106
|
+
if not isinstance(rects, list) or not rects:
|
|
107
|
+
raise ValueError("`position.rects` must be a non-empty list.")
|
|
108
|
+
return payload
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def _normalize_content(cls, value: object) -> dict[str, Any]:
|
|
112
|
+
payload = _json_clone(value if isinstance(value, dict) else {})
|
|
113
|
+
if not isinstance(payload, dict):
|
|
114
|
+
return {}
|
|
115
|
+
text = payload.get("text")
|
|
116
|
+
image = payload.get("image")
|
|
117
|
+
result: dict[str, Any] = {}
|
|
118
|
+
if isinstance(text, str):
|
|
119
|
+
result["text"] = text
|
|
120
|
+
if isinstance(image, str):
|
|
121
|
+
result["image"] = image
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def _normalize_author(color: object) -> dict[str, str]:
|
|
126
|
+
author_color = str(color or _DEFAULT_AUTHOR_COLOR).strip() or _DEFAULT_AUTHOR_COLOR
|
|
127
|
+
return {
|
|
128
|
+
"id": "local-user",
|
|
129
|
+
"handle": "user",
|
|
130
|
+
"color": author_color,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def _normalize_item(cls, item: dict[str, Any]) -> dict[str, Any]:
|
|
135
|
+
author = item.get("author") if isinstance(item.get("author"), dict) else {}
|
|
136
|
+
color = str(item.get("color") or author.get("color") or _DEFAULT_AUTHOR_COLOR).strip() or _DEFAULT_AUTHOR_COLOR
|
|
137
|
+
normalized = {
|
|
138
|
+
"id": str(item.get("id") or "").strip(),
|
|
139
|
+
"file_id": str(item.get("file_id") or "").strip(),
|
|
140
|
+
"project_id": str(item.get("project_id") or "").strip(),
|
|
141
|
+
"position": cls._normalize_position(item.get("position") or {}),
|
|
142
|
+
"content": cls._normalize_content(item.get("content") or {}),
|
|
143
|
+
"comment": str(item.get("comment") or "").strip(),
|
|
144
|
+
"kind": cls._normalize_kind(item.get("kind")),
|
|
145
|
+
"color": color,
|
|
146
|
+
"tags": cls._normalize_tags(item.get("tags")),
|
|
147
|
+
"created_by": str(item.get("created_by") or "local-user").strip() or "local-user",
|
|
148
|
+
"author": {
|
|
149
|
+
"id": str(author.get("id") or "local-user").strip() or "local-user",
|
|
150
|
+
"handle": str(author.get("handle") or "user").strip() or "user",
|
|
151
|
+
"color": str(author.get("color") or color).strip() or color,
|
|
152
|
+
},
|
|
153
|
+
"created_at": str(item.get("created_at") or utc_now()).strip() or utc_now(),
|
|
154
|
+
"updated_at": str(item.get("updated_at") or utc_now()).strip() or utc_now(),
|
|
155
|
+
}
|
|
156
|
+
if not normalized["id"]:
|
|
157
|
+
raise ValueError("Annotation id is required.")
|
|
158
|
+
if not normalized["file_id"]:
|
|
159
|
+
raise ValueError("Annotation file id is required.")
|
|
160
|
+
if not normalized["project_id"]:
|
|
161
|
+
raise ValueError("Annotation project id is required.")
|
|
162
|
+
return normalized
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _response_item(item: dict[str, Any]) -> dict[str, Any]:
|
|
166
|
+
return {
|
|
167
|
+
"id": item["id"],
|
|
168
|
+
"file_id": item["file_id"],
|
|
169
|
+
"project_id": item["project_id"],
|
|
170
|
+
"position": _json_clone(item["position"]),
|
|
171
|
+
"content": _json_clone(item["content"]),
|
|
172
|
+
"comment": item["comment"],
|
|
173
|
+
"kind": item["kind"],
|
|
174
|
+
"color": item["color"],
|
|
175
|
+
"tags": list(item["tags"]),
|
|
176
|
+
"created_by": item["created_by"],
|
|
177
|
+
"author": dict(item["author"]),
|
|
178
|
+
"created_at": item["created_at"],
|
|
179
|
+
"updated_at": item["updated_at"],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
def _ensure_document_exists(self, project_id: str, document_id: str) -> None:
|
|
183
|
+
quest_service = QuestService(self.home)
|
|
184
|
+
quest_service.open_document(project_id, document_id)
|
|
185
|
+
|
|
186
|
+
def list_annotations(self, file_id: str) -> dict[str, Any]:
|
|
187
|
+
project_id, document_id, _relative_path = self._parse_quest_file_id(file_id)
|
|
188
|
+
self._ensure_document_exists(project_id, document_id)
|
|
189
|
+
manifest = self._load_manifest(self._quest_root(project_id))
|
|
190
|
+
items = [
|
|
191
|
+
self._response_item(self._normalize_item(item))
|
|
192
|
+
for item in manifest["items"]
|
|
193
|
+
if str(item.get("file_id") or "").strip() == file_id
|
|
194
|
+
]
|
|
195
|
+
items.sort(key=lambda item: (item.get("created_at") or "", item.get("id") or ""), reverse=True)
|
|
196
|
+
return {"items": items, "total": len(items)}
|
|
197
|
+
|
|
198
|
+
def create_annotation(
|
|
199
|
+
self,
|
|
200
|
+
*,
|
|
201
|
+
file_id: str,
|
|
202
|
+
position: object,
|
|
203
|
+
content: object,
|
|
204
|
+
comment: object = "",
|
|
205
|
+
kind: object = "note",
|
|
206
|
+
color: object = None,
|
|
207
|
+
tags: object = None,
|
|
208
|
+
) -> dict[str, Any]:
|
|
209
|
+
project_id, document_id, _relative_path = self._parse_quest_file_id(file_id)
|
|
210
|
+
self._ensure_document_exists(project_id, document_id)
|
|
211
|
+
quest_root = self._quest_root(project_id)
|
|
212
|
+
author = self._normalize_author(color)
|
|
213
|
+
now = utc_now()
|
|
214
|
+
item = self._normalize_item(
|
|
215
|
+
{
|
|
216
|
+
"id": generate_id("ann"),
|
|
217
|
+
"file_id": file_id,
|
|
218
|
+
"project_id": project_id,
|
|
219
|
+
"position": position,
|
|
220
|
+
"content": content,
|
|
221
|
+
"comment": str(comment or "").strip(),
|
|
222
|
+
"kind": kind,
|
|
223
|
+
"color": author["color"],
|
|
224
|
+
"tags": tags,
|
|
225
|
+
"created_by": author["id"],
|
|
226
|
+
"author": author,
|
|
227
|
+
"created_at": now,
|
|
228
|
+
"updated_at": now,
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
with self._lock:
|
|
232
|
+
manifest = self._load_manifest(quest_root)
|
|
233
|
+
manifest["items"] = [*manifest["items"], item]
|
|
234
|
+
self._save_manifest(quest_root, manifest)
|
|
235
|
+
return self._response_item(item)
|
|
236
|
+
|
|
237
|
+
def _find_annotation(self, annotation_id: str) -> tuple[Path, dict[str, Any], list[dict[str, Any]], int]:
|
|
238
|
+
normalized_id = str(annotation_id or "").strip()
|
|
239
|
+
if not normalized_id:
|
|
240
|
+
raise FileNotFoundError("Unknown annotation.")
|
|
241
|
+
quests_root = self.home / "quests"
|
|
242
|
+
for quest_root in sorted(quests_root.iterdir()) if quests_root.exists() else []:
|
|
243
|
+
if not quest_root.is_dir():
|
|
244
|
+
continue
|
|
245
|
+
manifest = self._load_manifest(quest_root)
|
|
246
|
+
items = manifest["items"]
|
|
247
|
+
for index, raw_item in enumerate(items):
|
|
248
|
+
if str(raw_item.get("id") or "").strip() != normalized_id:
|
|
249
|
+
continue
|
|
250
|
+
return quest_root, manifest, items, index
|
|
251
|
+
raise FileNotFoundError(f"Unknown annotation `{normalized_id}`.")
|
|
252
|
+
|
|
253
|
+
def get_annotation(self, annotation_id: str) -> dict[str, Any]:
|
|
254
|
+
_quest_root, _manifest, items, index = self._find_annotation(annotation_id)
|
|
255
|
+
return self._response_item(self._normalize_item(items[index]))
|
|
256
|
+
|
|
257
|
+
def update_annotation(
|
|
258
|
+
self,
|
|
259
|
+
annotation_id: str,
|
|
260
|
+
*,
|
|
261
|
+
comment: object | None = None,
|
|
262
|
+
kind: object | None = None,
|
|
263
|
+
position: object | None = None,
|
|
264
|
+
content: object | None = None,
|
|
265
|
+
color: object | None = None,
|
|
266
|
+
tags: object | None = None,
|
|
267
|
+
) -> dict[str, Any]:
|
|
268
|
+
with self._lock:
|
|
269
|
+
quest_root, manifest, items, index = self._find_annotation(annotation_id)
|
|
270
|
+
current = self._normalize_item(items[index])
|
|
271
|
+
next_item = dict(current)
|
|
272
|
+
if comment is not None:
|
|
273
|
+
next_item["comment"] = str(comment or "").strip()
|
|
274
|
+
if kind is not None:
|
|
275
|
+
next_item["kind"] = self._normalize_kind(kind)
|
|
276
|
+
if position is not None:
|
|
277
|
+
next_item["position"] = self._normalize_position(position)
|
|
278
|
+
if content is not None:
|
|
279
|
+
next_item["content"] = self._normalize_content(content)
|
|
280
|
+
if color is not None:
|
|
281
|
+
resolved_color = str(color or current["color"]).strip() or current["color"]
|
|
282
|
+
next_item["color"] = resolved_color
|
|
283
|
+
next_item["author"] = {
|
|
284
|
+
**dict(current["author"]),
|
|
285
|
+
"color": resolved_color,
|
|
286
|
+
}
|
|
287
|
+
if tags is not None:
|
|
288
|
+
next_item["tags"] = self._normalize_tags(tags)
|
|
289
|
+
next_item["updated_at"] = utc_now()
|
|
290
|
+
normalized = self._normalize_item(next_item)
|
|
291
|
+
items[index] = normalized
|
|
292
|
+
manifest["items"] = items
|
|
293
|
+
self._save_manifest(quest_root, manifest)
|
|
294
|
+
return self._response_item(normalized)
|
|
295
|
+
|
|
296
|
+
def delete_annotation(self, annotation_id: str) -> dict[str, Any]:
|
|
297
|
+
with self._lock:
|
|
298
|
+
quest_root, manifest, items, index = self._find_annotation(annotation_id)
|
|
299
|
+
removed = self._normalize_item(items[index])
|
|
300
|
+
items.pop(index)
|
|
301
|
+
manifest["items"] = items
|
|
302
|
+
self._save_manifest(quest_root, manifest)
|
|
303
|
+
return {"ok": True, "id": removed["id"], "file_id": removed["file_id"], "project_id": removed["project_id"]}
|
|
304
|
+
|
|
305
|
+
def search_annotations(
|
|
306
|
+
self,
|
|
307
|
+
project_id: str,
|
|
308
|
+
*,
|
|
309
|
+
query: str | None = None,
|
|
310
|
+
color: str | None = None,
|
|
311
|
+
tag: str | None = None,
|
|
312
|
+
page: int | None = None,
|
|
313
|
+
limit: int = 100,
|
|
314
|
+
) -> dict[str, Any]:
|
|
315
|
+
quest_root = self._quest_root(project_id)
|
|
316
|
+
manifest = self._load_manifest(quest_root)
|
|
317
|
+
normalized_query = str(query or "").strip().lower()
|
|
318
|
+
normalized_color = str(color or "").strip().lower()
|
|
319
|
+
normalized_tag = str(tag or "").strip().lower()
|
|
320
|
+
items: list[dict[str, Any]] = []
|
|
321
|
+
for raw_item in manifest["items"]:
|
|
322
|
+
item = self._normalize_item(raw_item)
|
|
323
|
+
if normalized_color and str(item.get("color") or "").strip().lower() != normalized_color:
|
|
324
|
+
continue
|
|
325
|
+
if normalized_tag and normalized_tag not in {tag_item.lower() for tag_item in item.get("tags") or []}:
|
|
326
|
+
continue
|
|
327
|
+
if page is not None:
|
|
328
|
+
item_page = item.get("position", {}).get("pageNumber")
|
|
329
|
+
if item_page != page:
|
|
330
|
+
continue
|
|
331
|
+
if normalized_query:
|
|
332
|
+
haystack = " ".join(
|
|
333
|
+
[
|
|
334
|
+
str(item.get("comment") or ""),
|
|
335
|
+
str(item.get("content", {}).get("text") or ""),
|
|
336
|
+
" ".join(item.get("tags") or []),
|
|
337
|
+
]
|
|
338
|
+
).lower()
|
|
339
|
+
if normalized_query not in haystack:
|
|
340
|
+
continue
|
|
341
|
+
items.append(self._response_item(item))
|
|
342
|
+
items.sort(key=lambda item: (item.get("created_at") or "", item.get("id") or ""), reverse=True)
|
|
343
|
+
return {"items": items[: max(1, limit)], "total": len(items)}
|