@researai/deepscientist 1.5.15 → 1.5.16
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/README.md +336 -98
- package/bin/ds.js +691 -91
- package/docs/en/00_QUICK_START.md +36 -15
- package/docs/en/01_SETTINGS_REFERENCE.md +33 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
- package/docs/en/05_TUI_GUIDE.md +6 -0
- package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
- package/docs/en/09_DOCTOR.md +11 -5
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
- package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
- package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
- package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
- package/docs/en/README.md +18 -0
- package/docs/zh/00_QUICK_START.md +36 -15
- package/docs/zh/01_SETTINGS_REFERENCE.md +33 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
- package/docs/zh/05_TUI_GUIDE.md +6 -0
- package/docs/zh/09_DOCTOR.md +11 -5
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
- package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
- package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
- package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
- package/docs/zh/README.md +18 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/acp/envelope.py +6 -0
- package/src/deepscientist/artifact/service.py +647 -22
- package/src/deepscientist/bash_exec/service.py +234 -9
- package/src/deepscientist/cli.py +115 -19
- package/src/deepscientist/codex_cli_compat.py +232 -0
- package/src/deepscientist/config/models.py +2 -1
- package/src/deepscientist/config/service.py +31 -9
- package/src/deepscientist/daemon/api/handlers.py +125 -6
- package/src/deepscientist/daemon/api/router.py +4 -0
- package/src/deepscientist/daemon/app.py +715 -98
- package/src/deepscientist/gitops/__init__.py +10 -1
- package/src/deepscientist/gitops/diff.py +129 -0
- package/src/deepscientist/gitops/service.py +4 -1
- package/src/deepscientist/mcp/server.py +39 -0
- package/src/deepscientist/prompts/builder.py +255 -32
- package/src/deepscientist/quest/layout.py +15 -2
- package/src/deepscientist/quest/service.py +295 -43
- package/src/deepscientist/quest/stage_views.py +6 -1
- package/src/deepscientist/runners/codex.py +86 -31
- package/src/deepscientist/skills/__init__.py +2 -2
- package/src/deepscientist/skills/installer.py +196 -5
- package/src/deepscientist/skills/registry.py +66 -0
- package/src/prompts/connectors/qq.md +18 -8
- package/src/prompts/connectors/weixin.md +16 -6
- package/src/prompts/contracts/shared_interaction.md +12 -1
- package/src/prompts/system.md +10 -5
- package/src/prompts/system_copilot.md +43 -0
- package/src/skills/analysis-campaign/SKILL.md +1 -0
- package/src/skills/baseline/SKILL.md +8 -0
- package/src/skills/decision/SKILL.md +8 -0
- package/src/skills/experiment/SKILL.md +8 -0
- package/src/skills/figure-polish/SKILL.md +1 -0
- package/src/skills/finalize/SKILL.md +1 -0
- package/src/skills/idea/SKILL.md +1 -0
- package/src/skills/intake-audit/SKILL.md +8 -0
- package/src/skills/mentor/SKILL.md +217 -0
- package/src/skills/mentor/references/correction-rules.md +210 -0
- package/src/skills/mentor/references/knowledge-profile.md +91 -0
- package/src/skills/mentor/references/persona-profile.md +138 -0
- package/src/skills/mentor/references/taste-profile.md +128 -0
- package/src/skills/mentor/references/thought-style-profile.md +138 -0
- package/src/skills/mentor/references/work-profile.md +289 -0
- package/src/skills/mentor/references/workflow-profile.md +240 -0
- package/src/skills/optimize/SKILL.md +1 -0
- package/src/skills/rebuttal/SKILL.md +1 -0
- package/src/skills/review/SKILL.md +1 -0
- package/src/skills/scout/SKILL.md +8 -0
- package/src/skills/write/SKILL.md +1 -0
- package/src/tui/dist/app/AppContainer.js +19 -11
- package/src/tui/dist/index.js +4 -1
- package/src/tui/dist/lib/api.js +33 -3
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
- package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
- package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
- package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
- package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
- package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
- package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
- package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
- package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
- package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
- package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
- package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
- package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
- package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
- package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
- package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
- package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
- package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
- package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
- package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
- package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
- package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
- package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
- package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
- package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
- package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
- package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
- package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
- package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
- package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
- package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
- package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
- package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
- package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
- package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
- package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
- package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
- package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
- package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
- package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
- package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
- package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
- package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
- package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
- package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
- package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
- package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
- package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
- package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
- package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
- package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
- package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
- package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
- package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
- package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
- package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
- package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
- package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
- package/src/ui/dist/index.html +5 -2
- package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
- package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
- package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
- package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
- package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
- package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
- package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
- package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
- package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
- package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
- package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
- package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
- package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
- package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
- package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
- package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
- package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
- package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
- package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
- package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
- package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
- package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
- package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
- package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
- package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
- package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
- package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
- package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
- package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
- package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
- package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
- package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
- package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
- package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
- package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
- package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
- package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
- package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
- package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
- package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
- package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
- package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
- package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
- package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
- package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
- package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
- package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
- package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
- package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
- package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
- package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
- package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
|
@@ -2,12 +2,19 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
|
+
import shutil
|
|
5
6
|
import subprocess
|
|
6
7
|
import tomllib
|
|
7
8
|
from functools import lru_cache
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .shared import ensure_dir, read_text, write_text
|
|
8
12
|
|
|
9
13
|
_MIN_XHIGH_SUPPORTED_VERSION = (0, 63, 0)
|
|
10
14
|
_CODEX_VERSION_PATTERN = re.compile(r"codex-cli\s+(\d+)\.(\d+)\.(\d+)", re.IGNORECASE)
|
|
15
|
+
_CODEX_HOME_SYNCED_FILES = ("config.toml", "auth.json")
|
|
16
|
+
_CODEX_HOME_SYNCED_DIRS = ("skills", "agents", "prompts")
|
|
17
|
+
_CODEX_HOME_QUEST_OVERLAY_DIRS = ("skills", "prompts")
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
def parse_codex_cli_version(text: str) -> tuple[int, int, int] | None:
|
|
@@ -115,3 +122,228 @@ def adapt_profile_only_provider_config(
|
|
|
115
122
|
f"{', '.join(injected_fields)} to the top level for Codex compatibility."
|
|
116
123
|
),
|
|
117
124
|
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _remove_tree_path(path: Path) -> None:
|
|
128
|
+
if not path.exists() and not path.is_symlink():
|
|
129
|
+
return
|
|
130
|
+
if path.is_symlink() or path.is_file():
|
|
131
|
+
path.unlink()
|
|
132
|
+
return
|
|
133
|
+
shutil.rmtree(path)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _overlay_file_sources(*roots: Path) -> dict[Path, Path]:
|
|
137
|
+
merged: dict[Path, Path] = {}
|
|
138
|
+
for root in roots:
|
|
139
|
+
if not root.exists() or not root.is_dir():
|
|
140
|
+
continue
|
|
141
|
+
for source_path in sorted(root.rglob("*")):
|
|
142
|
+
if not source_path.is_file():
|
|
143
|
+
continue
|
|
144
|
+
merged[source_path.relative_to(root)] = source_path
|
|
145
|
+
return merged
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _sync_overlay_directory(target_dir: Path, *source_dirs: Path) -> None:
|
|
149
|
+
desired_files = _overlay_file_sources(*source_dirs)
|
|
150
|
+
if not desired_files:
|
|
151
|
+
_remove_tree_path(target_dir)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
desired_dirs: set[Path] = {Path(".")}
|
|
155
|
+
for relative in desired_files:
|
|
156
|
+
parent = relative.parent
|
|
157
|
+
while True:
|
|
158
|
+
desired_dirs.add(parent)
|
|
159
|
+
if parent == Path("."):
|
|
160
|
+
break
|
|
161
|
+
parent = parent.parent
|
|
162
|
+
|
|
163
|
+
if target_dir.exists() or target_dir.is_symlink():
|
|
164
|
+
for existing_path in sorted(target_dir.rglob("*"), reverse=True):
|
|
165
|
+
relative = existing_path.relative_to(target_dir)
|
|
166
|
+
if existing_path.is_dir() and not existing_path.is_symlink():
|
|
167
|
+
if relative not in desired_dirs:
|
|
168
|
+
shutil.rmtree(existing_path)
|
|
169
|
+
continue
|
|
170
|
+
if relative not in desired_files:
|
|
171
|
+
existing_path.unlink()
|
|
172
|
+
|
|
173
|
+
ensure_dir(target_dir)
|
|
174
|
+
for relative in sorted(desired_dirs, key=lambda item: (len(item.parts), item.as_posix())):
|
|
175
|
+
if relative == Path("."):
|
|
176
|
+
continue
|
|
177
|
+
current = target_dir / relative
|
|
178
|
+
if current.exists() and (current.is_symlink() or current.is_file()):
|
|
179
|
+
current.unlink()
|
|
180
|
+
ensure_dir(current)
|
|
181
|
+
|
|
182
|
+
for relative, source_path in desired_files.items():
|
|
183
|
+
target_path = target_dir / relative
|
|
184
|
+
if target_path.exists() and target_path.is_dir() and not target_path.is_symlink():
|
|
185
|
+
shutil.rmtree(target_path)
|
|
186
|
+
ensure_dir(target_path.parent)
|
|
187
|
+
try:
|
|
188
|
+
same_path = source_path.resolve() == target_path.resolve()
|
|
189
|
+
except FileNotFoundError:
|
|
190
|
+
same_path = False
|
|
191
|
+
if same_path:
|
|
192
|
+
continue
|
|
193
|
+
shutil.copy2(source_path, target_path)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def materialize_codex_runtime_home(
|
|
197
|
+
*,
|
|
198
|
+
source_home: str | Path,
|
|
199
|
+
target_home: str | Path,
|
|
200
|
+
profile: str = "",
|
|
201
|
+
quest_codex_root: str | Path | None = None,
|
|
202
|
+
) -> str | None:
|
|
203
|
+
source_root = Path(source_home).expanduser()
|
|
204
|
+
target_root = ensure_dir(Path(target_home))
|
|
205
|
+
|
|
206
|
+
for filename in _CODEX_HOME_SYNCED_FILES:
|
|
207
|
+
source_path = source_root / filename
|
|
208
|
+
target_path = target_root / filename
|
|
209
|
+
if not source_path.exists():
|
|
210
|
+
_remove_tree_path(target_path)
|
|
211
|
+
continue
|
|
212
|
+
if target_path.exists() and target_path.is_dir() and not target_path.is_symlink():
|
|
213
|
+
shutil.rmtree(target_path)
|
|
214
|
+
ensure_dir(target_path.parent)
|
|
215
|
+
try:
|
|
216
|
+
same_path = source_path.resolve() == target_path.resolve()
|
|
217
|
+
except FileNotFoundError:
|
|
218
|
+
same_path = False
|
|
219
|
+
if not same_path:
|
|
220
|
+
shutil.copy2(source_path, target_path)
|
|
221
|
+
|
|
222
|
+
overlay_root = Path(quest_codex_root) if quest_codex_root is not None else None
|
|
223
|
+
for dirname in _CODEX_HOME_SYNCED_DIRS:
|
|
224
|
+
overlay_dir = overlay_root / dirname if overlay_root is not None and dirname in _CODEX_HOME_QUEST_OVERLAY_DIRS else None
|
|
225
|
+
source_dirs: list[Path] = [source_root / dirname]
|
|
226
|
+
if overlay_dir is not None:
|
|
227
|
+
source_dirs.append(overlay_dir)
|
|
228
|
+
_sync_overlay_directory(target_root / dirname, *source_dirs)
|
|
229
|
+
|
|
230
|
+
warning: str | None = None
|
|
231
|
+
config_path = target_root / "config.toml"
|
|
232
|
+
if profile and config_path.exists():
|
|
233
|
+
adapted_text, warning = adapt_profile_only_provider_config(read_text(config_path), profile=profile)
|
|
234
|
+
write_text(config_path, adapted_text)
|
|
235
|
+
return warning
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def provider_profile_metadata(
|
|
239
|
+
config_text: str,
|
|
240
|
+
*,
|
|
241
|
+
profile: str,
|
|
242
|
+
) -> dict[str, str | bool | None]:
|
|
243
|
+
normalized_profile = str(profile or "").strip()
|
|
244
|
+
if not normalized_profile or not str(config_text or "").strip():
|
|
245
|
+
return {
|
|
246
|
+
"provider": None,
|
|
247
|
+
"model": None,
|
|
248
|
+
"env_key": None,
|
|
249
|
+
"base_url": None,
|
|
250
|
+
"wire_api": None,
|
|
251
|
+
"requires_openai_auth": None,
|
|
252
|
+
}
|
|
253
|
+
try:
|
|
254
|
+
parsed = tomllib.loads(config_text)
|
|
255
|
+
except tomllib.TOMLDecodeError:
|
|
256
|
+
return {
|
|
257
|
+
"provider": None,
|
|
258
|
+
"model": None,
|
|
259
|
+
"env_key": None,
|
|
260
|
+
"base_url": None,
|
|
261
|
+
"wire_api": None,
|
|
262
|
+
"requires_openai_auth": None,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
profiles = parsed.get("profiles")
|
|
266
|
+
if not isinstance(profiles, dict):
|
|
267
|
+
return {
|
|
268
|
+
"provider": None,
|
|
269
|
+
"model": None,
|
|
270
|
+
"env_key": None,
|
|
271
|
+
"base_url": None,
|
|
272
|
+
"wire_api": None,
|
|
273
|
+
"requires_openai_auth": None,
|
|
274
|
+
}
|
|
275
|
+
profile_payload = profiles.get(normalized_profile)
|
|
276
|
+
if not isinstance(profile_payload, dict):
|
|
277
|
+
return {
|
|
278
|
+
"provider": None,
|
|
279
|
+
"model": None,
|
|
280
|
+
"env_key": None,
|
|
281
|
+
"base_url": None,
|
|
282
|
+
"wire_api": None,
|
|
283
|
+
"requires_openai_auth": None,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
model_provider = str(
|
|
287
|
+
profile_payload.get("model_provider")
|
|
288
|
+
or parsed.get("model_provider")
|
|
289
|
+
or ""
|
|
290
|
+
).strip() or None
|
|
291
|
+
model = str(
|
|
292
|
+
profile_payload.get("model")
|
|
293
|
+
or parsed.get("model")
|
|
294
|
+
or ""
|
|
295
|
+
).strip() or None
|
|
296
|
+
provider_payload = None
|
|
297
|
+
model_providers = parsed.get("model_providers")
|
|
298
|
+
if model_provider and isinstance(model_providers, dict):
|
|
299
|
+
candidate = model_providers.get(model_provider)
|
|
300
|
+
if isinstance(candidate, dict):
|
|
301
|
+
provider_payload = candidate
|
|
302
|
+
|
|
303
|
+
env_key = (
|
|
304
|
+
str(provider_payload.get("env_key") or "").strip()
|
|
305
|
+
if isinstance(provider_payload, dict)
|
|
306
|
+
else None
|
|
307
|
+
) or None
|
|
308
|
+
base_url = (
|
|
309
|
+
str(provider_payload.get("base_url") or "").strip()
|
|
310
|
+
if isinstance(provider_payload, dict)
|
|
311
|
+
else None
|
|
312
|
+
) or None
|
|
313
|
+
wire_api = (
|
|
314
|
+
str(provider_payload.get("wire_api") or "").strip()
|
|
315
|
+
if isinstance(provider_payload, dict)
|
|
316
|
+
else None
|
|
317
|
+
) or None
|
|
318
|
+
requires_openai_auth = (
|
|
319
|
+
bool(provider_payload.get("requires_openai_auth"))
|
|
320
|
+
if isinstance(provider_payload, dict) and "requires_openai_auth" in provider_payload
|
|
321
|
+
else None
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
"provider": model_provider,
|
|
326
|
+
"model": model,
|
|
327
|
+
"env_key": env_key,
|
|
328
|
+
"base_url": base_url,
|
|
329
|
+
"wire_api": wire_api,
|
|
330
|
+
"requires_openai_auth": requires_openai_auth,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def provider_profile_metadata_from_home(
|
|
335
|
+
config_home: str | Path,
|
|
336
|
+
*,
|
|
337
|
+
profile: str,
|
|
338
|
+
) -> dict[str, str | bool | None]:
|
|
339
|
+
config_path = Path(config_home).expanduser() / "config.toml"
|
|
340
|
+
if not config_path.exists():
|
|
341
|
+
return {
|
|
342
|
+
"provider": None,
|
|
343
|
+
"model": None,
|
|
344
|
+
"env_key": None,
|
|
345
|
+
"base_url": None,
|
|
346
|
+
"wire_api": None,
|
|
347
|
+
"requires_openai_auth": None,
|
|
348
|
+
}
|
|
349
|
+
return provider_profile_metadata(config_path.read_text(encoding="utf-8"), profile=profile)
|
|
@@ -38,6 +38,7 @@ def default_config(home: Path) -> dict:
|
|
|
38
38
|
"ui": {
|
|
39
39
|
"host": "0.0.0.0",
|
|
40
40
|
"port": 20999,
|
|
41
|
+
"auth_enabled": False,
|
|
41
42
|
"auto_open_browser": True,
|
|
42
43
|
"default_mode": "web",
|
|
43
44
|
},
|
|
@@ -96,7 +97,7 @@ def default_runners() -> dict:
|
|
|
96
97
|
"binary": "codex",
|
|
97
98
|
"config_dir": "~/.codex",
|
|
98
99
|
"profile": "",
|
|
99
|
-
"model": "
|
|
100
|
+
"model": "inherit",
|
|
100
101
|
"model_reasoning_effort": "xhigh",
|
|
101
102
|
"approval_policy": "never",
|
|
102
103
|
"sandbox_mode": "danger-full-access",
|
|
@@ -4,13 +4,17 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import subprocess
|
|
6
6
|
import tempfile
|
|
7
|
-
from shutil import copy2
|
|
8
7
|
from copy import deepcopy
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
from urllib.error import URLError
|
|
11
10
|
from urllib.request import Request
|
|
12
11
|
|
|
13
|
-
from ..codex_cli_compat import
|
|
12
|
+
from ..codex_cli_compat import (
|
|
13
|
+
adapt_profile_only_provider_config,
|
|
14
|
+
materialize_codex_runtime_home,
|
|
15
|
+
normalize_codex_reasoning_effort,
|
|
16
|
+
provider_profile_metadata_from_home,
|
|
17
|
+
)
|
|
14
18
|
from ..connector.connector_profiles import PROFILEABLE_CONNECTOR_NAMES, list_connector_profiles, normalize_connector_config
|
|
15
19
|
from ..connector_runtime import build_discovered_target, infer_connector_transport
|
|
16
20
|
from ..home import repo_root
|
|
@@ -1192,6 +1196,14 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1192
1196
|
return "gpt-5.4"
|
|
1193
1197
|
return str(raw_model).strip()
|
|
1194
1198
|
|
|
1199
|
+
@staticmethod
|
|
1200
|
+
def _codex_effective_model(config: dict) -> str:
|
|
1201
|
+
requested = ConfigManager._codex_requested_model(config)
|
|
1202
|
+
profile = ConfigManager._codex_profile_name(config)
|
|
1203
|
+
if profile and not ConfigManager._codex_should_inherit_model(requested):
|
|
1204
|
+
return "inherit"
|
|
1205
|
+
return requested
|
|
1206
|
+
|
|
1195
1207
|
@staticmethod
|
|
1196
1208
|
def _codex_profile_name(config: dict) -> str:
|
|
1197
1209
|
raw_profile = config.get("profile")
|
|
@@ -1233,10 +1245,11 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1233
1245
|
|
|
1234
1246
|
temp_home = tempfile.TemporaryDirectory(prefix="ds-codex-probe-")
|
|
1235
1247
|
temp_root = Path(temp_home.name)
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1248
|
+
materialize_codex_runtime_home(
|
|
1249
|
+
source_home=expanded,
|
|
1250
|
+
target_home=temp_root,
|
|
1251
|
+
profile=profile,
|
|
1252
|
+
)
|
|
1240
1253
|
write_text(temp_root / "config.toml", adapted_text)
|
|
1241
1254
|
return str(temp_root), warning, temp_home
|
|
1242
1255
|
|
|
@@ -1361,6 +1374,7 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1361
1374
|
resolved_binary = resolve_runner_binary(binary, runner_name="codex")
|
|
1362
1375
|
profile = self._codex_profile_name(config)
|
|
1363
1376
|
requested_model = self._codex_requested_model(config)
|
|
1377
|
+
effective_model = self._codex_effective_model(config)
|
|
1364
1378
|
raw_reasoning_effort = config.get("model_reasoning_effort")
|
|
1365
1379
|
requested_reasoning_effort = (
|
|
1366
1380
|
str(raw_reasoning_effort).strip()
|
|
@@ -1376,9 +1390,9 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1376
1390
|
"resolved_binary": resolved_binary,
|
|
1377
1391
|
"config_dir": str(config.get("config_dir") or "~/.codex"),
|
|
1378
1392
|
"profile": profile,
|
|
1379
|
-
"model":
|
|
1393
|
+
"model": effective_model or "inherit",
|
|
1380
1394
|
"requested_model": requested_model or "inherit",
|
|
1381
|
-
"effective_model":
|
|
1395
|
+
"effective_model": effective_model or "inherit",
|
|
1382
1396
|
"approval_policy": str(config.get("approval_policy") or "on-request"),
|
|
1383
1397
|
"sandbox_mode": str(config.get("sandbox_mode") or "workspace-write"),
|
|
1384
1398
|
"reasoning_effort": reasoning_effort,
|
|
@@ -1416,9 +1430,17 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1416
1430
|
env["CODEX_HOME"] = prepared_home
|
|
1417
1431
|
if profile_config_warning:
|
|
1418
1432
|
compatibility_warnings.append(profile_config_warning)
|
|
1433
|
+
metadata = provider_profile_metadata_from_home(env.get("CODEX_HOME") or config_dir, profile=profile)
|
|
1434
|
+
if metadata.get("requires_openai_auth") is False:
|
|
1435
|
+
env.pop("OPENAI_API_KEY", None)
|
|
1436
|
+
env.pop("OPENAI_BASE_URL", None)
|
|
1419
1437
|
prompt = "Reply with exactly HELLO."
|
|
1420
1438
|
if reasoning_effort_warning:
|
|
1421
1439
|
compatibility_warnings.append(reasoning_effort_warning)
|
|
1440
|
+
if profile and effective_model == "inherit" and not self._codex_should_inherit_model(requested_model):
|
|
1441
|
+
compatibility_warnings.append(
|
|
1442
|
+
f"Codex profile `{profile}` is provider-backed. DeepScientist is probing it with `model: inherit`."
|
|
1443
|
+
)
|
|
1422
1444
|
base_warnings: list[str] = list(compatibility_warnings)
|
|
1423
1445
|
|
|
1424
1446
|
def run_probe_once(model_for_command: str) -> tuple[list[str], subprocess.CompletedProcess[str] | None, subprocess.TimeoutExpired | None]:
|
|
@@ -1445,7 +1467,7 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1445
1467
|
return command, None, exc
|
|
1446
1468
|
return command, result, None
|
|
1447
1469
|
|
|
1448
|
-
command, result, timeout_error = run_probe_once(
|
|
1470
|
+
command, result, timeout_error = run_probe_once(effective_model)
|
|
1449
1471
|
if timeout_error is not None:
|
|
1450
1472
|
details.update(
|
|
1451
1473
|
{
|
|
@@ -17,6 +17,11 @@ from ...quest import QuestService
|
|
|
17
17
|
from ...shared import generate_id, read_json, read_text, resolve_within, run_command, sha256_text, utc_now
|
|
18
18
|
from ...runners import RunRequest
|
|
19
19
|
|
|
20
|
+
_COPILOT_LEAD_MESSAGE = (
|
|
21
|
+
"我是 DeepScientist,任何事情都可以找我帮忙。"
|
|
22
|
+
"你可以让我读论文、改代码、看实验、整理思路,或者直接开始执行一个任务。"
|
|
23
|
+
)
|
|
24
|
+
|
|
20
25
|
|
|
21
26
|
class ApiHandlers:
|
|
22
27
|
def __init__(self, app: "DaemonApp") -> None:
|
|
@@ -73,6 +78,8 @@ class ApiHandlers:
|
|
|
73
78
|
runtime_payload = {
|
|
74
79
|
"surface": "quest",
|
|
75
80
|
"version": DEEPSCIENTIST_VERSION,
|
|
81
|
+
"homePath": str(self.app.home),
|
|
82
|
+
"auth": self.app.browser_auth_runtime_payload(),
|
|
76
83
|
"supports": {
|
|
77
84
|
"productApis": False,
|
|
78
85
|
"socketIo": False,
|
|
@@ -158,9 +165,87 @@ npm --prefix src/ui run build</pre>
|
|
|
158
165
|
"daemon_id": self.app.daemon_id,
|
|
159
166
|
"managed_by": self.app.daemon_managed_by,
|
|
160
167
|
"pid": os.getpid(),
|
|
168
|
+
"auth_enabled": self.app.browser_auth_enabled,
|
|
161
169
|
"sessions": self.app.sessions.snapshot(),
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
def auth_login(self, body: dict | None = None) -> tuple[int, dict, str] | tuple[int, dict]:
|
|
173
|
+
if not self.app.browser_auth_enabled:
|
|
174
|
+
payload = {
|
|
175
|
+
"ok": True,
|
|
176
|
+
"authenticated": True,
|
|
177
|
+
"auth_enabled": False,
|
|
178
|
+
}
|
|
179
|
+
return 200, {"Content-Type": "application/json; charset=utf-8"}, json.dumps(payload, ensure_ascii=False)
|
|
180
|
+
|
|
181
|
+
candidate = str(((body or {}) if isinstance(body, dict) else {}).get("token") or "").strip()
|
|
182
|
+
if not candidate:
|
|
183
|
+
return 400, {
|
|
184
|
+
"ok": False,
|
|
185
|
+
"message": "Token is required.",
|
|
186
|
+
"auth_required": True,
|
|
187
|
+
"auth_enabled": True,
|
|
188
|
+
}
|
|
189
|
+
if not self.app.browser_auth_matches(candidate):
|
|
190
|
+
return 401, {
|
|
191
|
+
"ok": False,
|
|
192
|
+
"message": "Invalid token.",
|
|
193
|
+
"auth_required": True,
|
|
194
|
+
"auth_enabled": True,
|
|
195
|
+
}
|
|
196
|
+
payload = {
|
|
197
|
+
"ok": True,
|
|
198
|
+
"authenticated": True,
|
|
199
|
+
"auth_enabled": True,
|
|
200
|
+
"token_masked": self.app.masked_browser_auth_token(),
|
|
201
|
+
}
|
|
202
|
+
return (
|
|
203
|
+
200,
|
|
204
|
+
{
|
|
205
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
206
|
+
"Cache-Control": "no-store, max-age=0, must-revalidate",
|
|
207
|
+
"Set-Cookie": self.app._browser_auth_cookie_header(candidate),
|
|
208
|
+
},
|
|
209
|
+
json.dumps(payload, ensure_ascii=False),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def auth_token(self) -> dict:
|
|
213
|
+
return {
|
|
214
|
+
"ok": True,
|
|
215
|
+
"auth_enabled": self.app.browser_auth_enabled,
|
|
216
|
+
"token": self.app.browser_auth_token,
|
|
217
|
+
"token_masked": self.app.masked_browser_auth_token(),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
def auth_rotate(self, body: dict | None = None) -> tuple[int, dict, str] | tuple[int, dict]:
|
|
221
|
+
if not self.app.browser_auth_enabled:
|
|
222
|
+
payload = {
|
|
223
|
+
"ok": True,
|
|
224
|
+
"auth_enabled": False,
|
|
225
|
+
"rotated": False,
|
|
226
|
+
"token": None,
|
|
227
|
+
"token_masked": None,
|
|
228
|
+
}
|
|
229
|
+
return 200, {"Content-Type": "application/json; charset=utf-8"}, json.dumps(payload, ensure_ascii=False)
|
|
230
|
+
|
|
231
|
+
rotated = self.app.rotate_browser_auth_token()
|
|
232
|
+
payload = {
|
|
233
|
+
"ok": True,
|
|
234
|
+
"auth_enabled": True,
|
|
235
|
+
"rotated": True,
|
|
236
|
+
"token": rotated,
|
|
237
|
+
"token_masked": self.app.masked_browser_auth_token(),
|
|
238
|
+
}
|
|
239
|
+
return (
|
|
240
|
+
200,
|
|
241
|
+
{
|
|
242
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
243
|
+
"Cache-Control": "no-store, max-age=0, must-revalidate",
|
|
244
|
+
"Set-Cookie": self.app._browser_auth_cookie_header(rotated),
|
|
245
|
+
},
|
|
246
|
+
json.dumps(payload, ensure_ascii=False),
|
|
247
|
+
)
|
|
248
|
+
|
|
164
249
|
def system_update(self) -> dict:
|
|
165
250
|
return self.app.system_update_status()
|
|
166
251
|
|
|
@@ -324,6 +409,33 @@ npm --prefix src/ui run build</pre>
|
|
|
324
409
|
return 400, {"ok": False, "message": str(exc)}
|
|
325
410
|
except RuntimeError as exc:
|
|
326
411
|
return 409, {"ok": False, "message": str(exc)}
|
|
412
|
+
workspace_mode = (
|
|
413
|
+
str(startup_contract.get("workspace_mode") or "").strip().lower()
|
|
414
|
+
if isinstance(startup_contract, dict)
|
|
415
|
+
else ""
|
|
416
|
+
)
|
|
417
|
+
if workspace_mode in {"copilot", "autonomous"}:
|
|
418
|
+
quest_root = self.app.quest_service._quest_root(snapshot["quest_id"])
|
|
419
|
+
self.app.quest_service.update_research_state(quest_root, workspace_mode=workspace_mode)
|
|
420
|
+
if workspace_mode == "copilot":
|
|
421
|
+
self.app.quest_service.append_message(
|
|
422
|
+
snapshot["quest_id"],
|
|
423
|
+
"assistant",
|
|
424
|
+
_COPILOT_LEAD_MESSAGE,
|
|
425
|
+
source="deepscientist",
|
|
426
|
+
)
|
|
427
|
+
self.app.quest_service.update_runtime_state(
|
|
428
|
+
quest_root=quest_root,
|
|
429
|
+
status="idle",
|
|
430
|
+
display_status="idle",
|
|
431
|
+
)
|
|
432
|
+
self.app.quest_service.set_continuation_state(
|
|
433
|
+
quest_root,
|
|
434
|
+
policy="wait_for_user_or_resume",
|
|
435
|
+
anchor="decision",
|
|
436
|
+
reason="copilot_mode",
|
|
437
|
+
)
|
|
438
|
+
snapshot = self.app.quest_service.snapshot(snapshot["quest_id"])
|
|
327
439
|
payload: dict[str, object] = {"ok": True, "snapshot": snapshot}
|
|
328
440
|
if auto_start:
|
|
329
441
|
startup = self.app.submit_user_message(
|
|
@@ -474,11 +586,12 @@ npm --prefix src/ui run build</pre>
|
|
|
474
586
|
|
|
475
587
|
def quest_session(self, quest_id: str) -> dict:
|
|
476
588
|
snapshot = self.app.quest_service.snapshot_fast(quest_id)
|
|
477
|
-
for kind in ("details", "canvas"):
|
|
589
|
+
for kind in ("details", "canvas", "git_canvas"):
|
|
478
590
|
try:
|
|
479
591
|
self.app.quest_service.prime_projection(quest_id, kind)
|
|
480
592
|
except Exception:
|
|
481
593
|
continue
|
|
594
|
+
self.app.schedule_latest_quest_terminal_prewarm(quest_id)
|
|
482
595
|
return {
|
|
483
596
|
"ok": True,
|
|
484
597
|
"quest_id": quest_id,
|
|
@@ -496,7 +609,7 @@ npm --prefix src/ui run build</pre>
|
|
|
496
609
|
tail = tail_raw in {"1", "true", "yes", "on"}
|
|
497
610
|
format_name = ((query.get("format") or ["both"])[0] or "both").lower()
|
|
498
611
|
session_id = ((query.get("session_id") or [f"quest:{quest_id}"])[0] or f"quest:{quest_id}")
|
|
499
|
-
payload = self.
|
|
612
|
+
payload = self.app.quest_service.events(
|
|
500
613
|
quest_id,
|
|
501
614
|
after=after,
|
|
502
615
|
before=before,
|
|
@@ -1028,6 +1141,15 @@ npm --prefix src/ui run build</pre>
|
|
|
1028
1141
|
node["optimization_candidate_count"] = candidate_count_by_branch.get(ref, 0)
|
|
1029
1142
|
return payload
|
|
1030
1143
|
|
|
1144
|
+
def git_canvas(self, quest_id: str) -> dict:
|
|
1145
|
+
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
1146
|
+
payload = self.app.quest_service.git_commit_canvas(quest_id)
|
|
1147
|
+
research_state = self.app.quest_service.read_research_state(quest_root)
|
|
1148
|
+
active_workspace_branch = str(research_state.get("current_workspace_branch") or "").strip() or None
|
|
1149
|
+
payload["active_workspace_ref"] = active_workspace_branch
|
|
1150
|
+
payload["workspace_mode"] = str(research_state.get("workspace_mode") or "copilot").strip() or "copilot"
|
|
1151
|
+
return payload
|
|
1152
|
+
|
|
1031
1153
|
def git_log(self, quest_id: str, path: str) -> dict:
|
|
1032
1154
|
query = self.parse_query(path)
|
|
1033
1155
|
ref = ((query.get("ref") or [""])[0] or "").strip()
|
|
@@ -1390,10 +1512,7 @@ npm --prefix src/ui run build</pre>
|
|
|
1390
1512
|
mime_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
|
|
1391
1513
|
content = quest_service._read_git_bytes(quest_root, revision, relative)
|
|
1392
1514
|
return 200, self._asset_headers(mime_type), content
|
|
1393
|
-
path, _writable, _scope, _source_kind = quest_service.
|
|
1394
|
-
quest_service._quest_root(quest_id),
|
|
1395
|
-
document_id,
|
|
1396
|
-
)
|
|
1515
|
+
path, _writable, _scope, _source_kind = quest_service.resolve_document(quest_id, document_id)
|
|
1397
1516
|
if not path.exists() or not path.is_file():
|
|
1398
1517
|
return 404, {"Content-Type": "text/plain; charset=utf-8"}, b"Not Found"
|
|
1399
1518
|
mime_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
|
|
@@ -9,6 +9,9 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
|
|
|
9
9
|
("GET", re.compile(r"^/metis/agent/api/health$"), "lingzhu_health"),
|
|
10
10
|
("POST", re.compile(r"^/metis/agent/api/sse$"), "lingzhu_sse"),
|
|
11
11
|
("GET", re.compile(r"^/(?P<spa_path>(?!api(?:/|$)|metis(?:/|$)|ui(?:/|$)|assets(?:/|$)).+)$"), "spa_root"),
|
|
12
|
+
("POST", re.compile(r"^/api/auth/login$"), "auth_login"),
|
|
13
|
+
("GET", re.compile(r"^/api/auth/token$"), "auth_token"),
|
|
14
|
+
("POST", re.compile(r"^/api/auth/rotate$"), "auth_rotate"),
|
|
12
15
|
("GET", re.compile(r"^/api/health$"), "health"),
|
|
13
16
|
("GET", re.compile(r"^/api/system/update$"), "system_update"),
|
|
14
17
|
("POST", re.compile(r"^/api/system/update$"), "system_update_action"),
|
|
@@ -63,6 +66,7 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
|
|
|
63
66
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/metrics/timeline$"), "metrics_timeline"),
|
|
64
67
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/baselines/compare$"), "baseline_compare"),
|
|
65
68
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/branches$"), "git_branches"),
|
|
69
|
+
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/canvas$"), "git_canvas"),
|
|
66
70
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/log$"), "git_log"),
|
|
67
71
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/compare$"), "git_compare"),
|
|
68
72
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/commit$"), "git_commit"),
|