@researai/deepscientist 1.5.12 → 1.5.14
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/bin/ds.js +20 -3
- package/docs/en/00_QUICK_START.md +24 -5
- package/docs/en/01_SETTINGS_REFERENCE.md +4 -0
- package/docs/en/05_TUI_GUIDE.md +466 -96
- package/docs/en/09_DOCTOR.md +24 -5
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +113 -15
- package/docs/en/README.md +2 -0
- package/docs/zh/00_QUICK_START.md +24 -5
- package/docs/zh/01_SETTINGS_REFERENCE.md +4 -0
- package/docs/zh/05_TUI_GUIDE.md +465 -82
- package/docs/zh/09_DOCTOR.md +24 -5
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +113 -15
- package/docs/zh/README.md +2 -0
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/artifact/service.py +125 -2
- package/src/deepscientist/cli.py +3 -0
- package/src/deepscientist/codex_cli_compat.py +117 -0
- package/src/deepscientist/config/service.py +53 -6
- package/src/deepscientist/connector/lingzhu_support.py +23 -4
- package/src/deepscientist/daemon/app.py +111 -30
- package/src/deepscientist/mcp/server.py +161 -19
- package/src/deepscientist/prompts/builder.py +13 -54
- package/src/deepscientist/quest/service.py +99 -0
- package/src/deepscientist/quest/stage_views.py +134 -29
- package/src/deepscientist/runners/codex.py +11 -2
- package/src/deepscientist/runners/runtime_overrides.py +3 -0
- package/src/deepscientist/shared.py +6 -1
- package/src/prompts/system.md +220 -2065
- package/src/skills/baseline/SKILL.md +265 -994
- package/src/skills/baseline/references/artifact-payload-examples.md +39 -0
- package/src/skills/baseline/references/baseline-checklist-template.md +21 -32
- package/src/skills/baseline/references/baseline-plan-template.md +41 -57
- package/src/tui/dist/app/AppContainer.js +1442 -52
- package/src/tui/dist/components/Composer.js +1 -1
- package/src/tui/dist/components/ConfigScreen.js +190 -36
- package/src/tui/dist/components/GradientStatusText.js +1 -20
- package/src/tui/dist/components/InputPrompt.js +41 -32
- package/src/tui/dist/components/LoadingIndicator.js +1 -1
- package/src/tui/dist/components/Logo.js +61 -38
- package/src/tui/dist/components/MainContent.js +10 -3
- package/src/tui/dist/components/WelcomePanel.js +4 -12
- package/src/tui/dist/components/messages/AssistantMessage.js +1 -1
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -3
- package/src/tui/dist/components/messages/OperationMessage.js +1 -1
- package/src/tui/dist/index.js +28 -1
- package/src/tui/dist/layouts/DefaultAppLayout.js +3 -3
- package/src/tui/dist/lib/api.js +17 -0
- package/src/tui/dist/lib/connectorConfig.js +90 -0
- package/src/tui/dist/lib/connectors.js +261 -0
- package/src/tui/dist/lib/qr.js +21 -0
- package/src/tui/dist/semantic-colors.js +29 -19
- package/src/tui/package.json +2 -1
- package/src/ui/dist/assets/{AiManusChatView-CnJcXynW.js → AiManusChatView-DaF9Nge_.js} +12 -12
- package/src/ui/dist/assets/{AnalysisPlugin-DeyzPEhV.js → AnalysisPlugin-BSVx6dXE.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-CB1YODQn.js → CliPlugin-C9gzJX41.js} +9 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-B-xicq1e.js → CodeEditorPlugin-DU9G0Tox.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DT54ysXa.js → CodeViewerPlugin-DoX_fI9l.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-DQtKT-VD.js → DocViewerPlugin-C4FWIXuU.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-hqHbCfnv.js → GitDiffViewerPlugin-BgfFMgtf.js} +20 -20
- package/src/ui/dist/assets/{ImageViewerPlugin-OcVo33jV.js → ImageViewerPlugin-tcPkfY_x.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-DdGwhEUV.js → LabCopilotPanel-_dKV60Bf.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-Ciz1gDaX.js → LabPlugin-Bje0ayoC.js} +2 -2
- package/src/ui/dist/assets/{LatexPlugin-BhmjNQRC.js → LatexPlugin-CVsBzAln.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-BzdVH9Bx.js → MarkdownViewerPlugin-xjmrqv_8.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DmyHspXt.js → MarketplacePlugin-mMM2A8wP.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-BTVYRGkm.js → NotebookEditor-3kVDSOBo.js} +11 -11
- package/src/ui/dist/assets/{NotebookEditor-BMXKrDRk.js → NotebookEditor-SoJ8X-MO.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-CvcjJHXv.js → PdfLoader-DElVuHl9.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DW2ej8Vk.js → PdfMarkdownPlugin-Bq88XT4G.js} +2 -2
- package/src/ui/dist/assets/{PdfViewerPlugin-CmlDxbhU.js → PdfViewerPlugin-CsCXMo9S.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-DAjQZPSv.js → SearchPlugin-oUPvy19k.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-C-nVAZb_.js → TextViewerPlugin-CRkT9yNy.js} +5 -5
- package/src/ui/dist/assets/{VNCViewer-D7-dIYon.js → VNCViewer-BgbuvWhR.js} +10 -10
- package/src/ui/dist/assets/{bot-C_G4WtNI.js → bot-v_RASACv.js} +1 -1
- package/src/ui/dist/assets/{code-Cd7WfiWq.js → code-5hC9d0VH.js} +1 -1
- package/src/ui/dist/assets/{file-content-B57zsL9y.js → file-content-D1PxfOrp.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DVoheLFq.js → file-diff-panel-DG1oT_Hj.js} +1 -1
- package/src/ui/dist/assets/{file-socket-B5kXFxZP.js → file-socket-BmdFYQlk.js} +1 -1
- package/src/ui/dist/assets/{image-LLOjkMHF.js → image-Dqe2X2tW.js} +1 -1
- package/src/ui/dist/assets/{index-Dxa2eYMY.js → index-DVsMKK_y.js} +1 -1
- package/src/ui/dist/assets/{index-C3r2iGrp.js → index-Duvz8Ip0.js} +12 -12
- package/src/ui/dist/assets/{index-CLQauncb.js → index-Nt9hS4ck.js} +470 -165
- package/src/ui/dist/assets/{index-hOUOWbW2.js → index-RDlNXXx1.js} +2 -2
- package/src/ui/dist/assets/{monaco-BGGAEii3.js → monaco-DIXge1CP.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DlEr1_y5.js → pdf-effect-queue-BBTTQaO-.js} +1 -1
- package/src/ui/dist/assets/{popover-CWJbJuYY.js → popover-BWlolyxo.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CRJiucYO.js → project-sync-BM5PkFH4.js} +1 -1
- package/src/ui/dist/assets/{select-CoHB7pvH.js → select-D4dAtrA8.js} +2 -2
- package/src/ui/dist/assets/{sigma-D5aJWR8J.js → sigma-CKbE5jJT.js} +1 -1
- package/src/ui/dist/assets/{square-check-big-DUK_mnkS.js → square-check-big-CZNGMgiB.js} +1 -1
- package/src/ui/dist/assets/{trash-ChU3SEE3.js → trash-DaB37xAz.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-BrJBV3tY.js → useCliAccess-C2OmAcWe.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-C2OQaVWc.js → useFileDiffOverlay-Dowd1Ij4.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-C7Qqh-om.js → wrap-text-BGjAhAUq.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-rtX0FKya.js → zoom-out-dMZQMXzc.js} +1 -1
- package/src/ui/dist/index.html +1 -1
- package/uv.lock +1 -1
|
@@ -15,6 +15,9 @@ DEFAULT_LINGZHU_AGENT_ID = "main"
|
|
|
15
15
|
DEFAULT_LINGZHU_SESSION_NAMESPACE = "lingzhu"
|
|
16
16
|
DEFAULT_LINGZHU_TASK_PREFIX = "我现在的任务是"
|
|
17
17
|
DEFAULT_LINGZHU_PASSIVE_CHAT_TYPE = "passive"
|
|
18
|
+
_LINGZHU_COMMAND_PUNCTUATION = "::,,。.;;!!??、~~`'\"“”‘’()()【】[]《》<>"
|
|
19
|
+
_LINGZHU_COMMAND_PUNCT_TRANSLATION = str.maketrans({char: " " for char in _LINGZHU_COMMAND_PUNCTUATION})
|
|
20
|
+
_LINGZHU_PREFIX_SEPARATORS_CLASS = re.escape(_LINGZHU_COMMAND_PUNCTUATION) + r"\s"
|
|
18
21
|
|
|
19
22
|
_AUTH_AK_SEGMENTS = (8, 4, 4, 4, 12)
|
|
20
23
|
_AUTH_AK_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
@@ -237,12 +240,28 @@ def lingzhu_extract_user_text(messages: Any) -> str:
|
|
|
237
240
|
return "\n".join(parts).strip()
|
|
238
241
|
|
|
239
242
|
|
|
240
|
-
def
|
|
243
|
+
def lingzhu_normalize_command_text(text: Any) -> str:
|
|
241
244
|
normalized = str(text or "").strip()
|
|
242
|
-
if not normalized
|
|
245
|
+
if not normalized:
|
|
246
|
+
return ""
|
|
247
|
+
normalized = normalized.translate(_LINGZHU_COMMAND_PUNCT_TRANSLATION)
|
|
248
|
+
normalized = re.sub(r"\s+", " ", normalized)
|
|
249
|
+
return normalized.strip()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def lingzhu_extract_task_text(text: Any) -> str | None:
|
|
253
|
+
raw_text = str(text or "").strip()
|
|
254
|
+
if not raw_text:
|
|
255
|
+
return None
|
|
256
|
+
prefix_pattern = r"^[{separators}]*{prefix}[{separators}]*".format(
|
|
257
|
+
separators=_LINGZHU_PREFIX_SEPARATORS_CLASS,
|
|
258
|
+
prefix="".join(f"{re.escape(char)}[{_LINGZHU_PREFIX_SEPARATORS_CLASS}]*" for char in DEFAULT_LINGZHU_TASK_PREFIX),
|
|
259
|
+
)
|
|
260
|
+
matched = re.match(prefix_pattern, raw_text)
|
|
261
|
+
if matched is None:
|
|
243
262
|
return None
|
|
244
|
-
remainder =
|
|
245
|
-
remainder = remainder.lstrip("
|
|
263
|
+
remainder = raw_text[matched.end() :].strip()
|
|
264
|
+
remainder = remainder.lstrip(_LINGZHU_COMMAND_PUNCTUATION + " \t\r\n")
|
|
246
265
|
return remainder or None
|
|
247
266
|
|
|
248
267
|
|
|
@@ -54,6 +54,7 @@ from ..network import urlopen_with_proxy as urlopen
|
|
|
54
54
|
from ..latex_runtime import QuestLatexService
|
|
55
55
|
from ..connector.lingzhu_support import (
|
|
56
56
|
lingzhu_detect_tool_call_from_text,
|
|
57
|
+
lingzhu_normalize_command_text,
|
|
57
58
|
lingzhu_extract_task_text,
|
|
58
59
|
lingzhu_extract_user_text,
|
|
59
60
|
lingzhu_health_payload,
|
|
@@ -102,6 +103,25 @@ LEGACY_CODEX_RETRY_BACKOFF_MULTIPLIER = 2.0
|
|
|
102
103
|
LEGACY_CODEX_RETRY_MAX_BACKOFF_SEC = 8.0
|
|
103
104
|
_CRASH_AUTO_RESUME_COOLDOWN = timedelta(minutes=10)
|
|
104
105
|
_CRASH_AUTO_RESUME_MAX_RECENT_ATTEMPTS = 2
|
|
106
|
+
_CHINESE_DIGIT_MAP = {
|
|
107
|
+
"零": 0,
|
|
108
|
+
"〇": 0,
|
|
109
|
+
"一": 1,
|
|
110
|
+
"二": 2,
|
|
111
|
+
"两": 2,
|
|
112
|
+
"三": 3,
|
|
113
|
+
"四": 4,
|
|
114
|
+
"五": 5,
|
|
115
|
+
"六": 6,
|
|
116
|
+
"七": 7,
|
|
117
|
+
"八": 8,
|
|
118
|
+
"九": 9,
|
|
119
|
+
}
|
|
120
|
+
_CHINESE_UNIT_MAP = {
|
|
121
|
+
"十": 10,
|
|
122
|
+
"百": 100,
|
|
123
|
+
"千": 1000,
|
|
124
|
+
}
|
|
105
125
|
_LINGZHU_SHORT_COMMAND_DIRECT_MAP = {
|
|
106
126
|
"帮助": "help",
|
|
107
127
|
"列表": "list",
|
|
@@ -117,7 +137,7 @@ _LINGZHU_SHORT_COMMAND_PREFIX_MAP = {
|
|
|
117
137
|
"暂停": "stop",
|
|
118
138
|
"恢复": "resume",
|
|
119
139
|
}
|
|
120
|
-
_LINGZHU_SHORT_LATEST_ALIASES = {"latest", "newest", "最新"}
|
|
140
|
+
_LINGZHU_SHORT_LATEST_ALIASES = {"latest", "newest", "最新", "最新的"}
|
|
121
141
|
_LINGZHU_DELETE_CONFIRM_ALIASES = {"确认", "强制", "--yes", "-y"}
|
|
122
142
|
|
|
123
143
|
|
|
@@ -3570,10 +3590,8 @@ class DaemonApp:
|
|
|
3570
3590
|
),
|
|
3571
3591
|
}
|
|
3572
3592
|
)
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
quests = self.quest_service.list_quests()
|
|
3576
|
-
target_quest = str(quests[0]["quest_id"]) if quests else ""
|
|
3593
|
+
requested_quest_ref = str(args[0] or "").strip()
|
|
3594
|
+
target_quest = self._resolve_quest_reference(requested_quest_ref) or requested_quest_ref
|
|
3577
3595
|
if not (self.home / "quests" / target_quest / "quest.yaml").exists():
|
|
3578
3596
|
available = ", ".join(item["quest_id"] for item in self.quest_service.list_quests()[:6]) or "none"
|
|
3579
3597
|
return channel.send(
|
|
@@ -3581,8 +3599,8 @@ class DaemonApp:
|
|
|
3581
3599
|
"conversation_id": conversation_id,
|
|
3582
3600
|
"kind": "ack",
|
|
3583
3601
|
"message": self._polite_copy(
|
|
3584
|
-
zh=f"老师,目前没有找到 quest `{
|
|
3585
|
-
en=f"I could not find quest `{
|
|
3602
|
+
zh=f"老师,目前没有找到 quest `{requested_quest_ref}`。可用 quest 包括:{available}。",
|
|
3603
|
+
en=f"I could not find quest `{requested_quest_ref}`. Available quests: {available}.",
|
|
3586
3604
|
),
|
|
3587
3605
|
}
|
|
3588
3606
|
)
|
|
@@ -3626,10 +3644,8 @@ class DaemonApp:
|
|
|
3626
3644
|
),
|
|
3627
3645
|
}
|
|
3628
3646
|
)
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
quests = self.quest_service.list_quests()
|
|
3632
|
-
target_quest = str(quests[0]["quest_id"]) if quests else ""
|
|
3647
|
+
requested_quest_ref = str(args[0] or "").strip()
|
|
3648
|
+
target_quest = self._resolve_quest_reference(requested_quest_ref) or requested_quest_ref
|
|
3633
3649
|
confirmed = any(str(item).strip().lower() in {"--yes", "--force", "-y"} for item in args[1:])
|
|
3634
3650
|
if not confirmed:
|
|
3635
3651
|
return channel.send(
|
|
@@ -3660,8 +3676,8 @@ class DaemonApp:
|
|
|
3660
3676
|
"conversation_id": conversation_id,
|
|
3661
3677
|
"kind": "ack",
|
|
3662
3678
|
"message": self._polite_copy(
|
|
3663
|
-
zh=f"老师,目前没有找到 quest `{
|
|
3664
|
-
en=f"I could not find quest `{
|
|
3679
|
+
zh=f"老师,目前没有找到 quest `{requested_quest_ref}`。可用 quest 包括:{available}。",
|
|
3680
|
+
en=f"I could not find quest `{requested_quest_ref}`. Available quests: {available}.",
|
|
3665
3681
|
),
|
|
3666
3682
|
}
|
|
3667
3683
|
)
|
|
@@ -4345,6 +4361,53 @@ class DaemonApp:
|
|
|
4345
4361
|
quest_id = str(quests[0].get("quest_id") or "").strip()
|
|
4346
4362
|
return quest_id or None
|
|
4347
4363
|
|
|
4364
|
+
@staticmethod
|
|
4365
|
+
def _strip_quest_reference_noise(value: str | None) -> str:
|
|
4366
|
+
normalized = lingzhu_normalize_command_text(value)
|
|
4367
|
+
if not normalized:
|
|
4368
|
+
return ""
|
|
4369
|
+
normalized = re.sub(r"^(?:第|quest|Quest|任务|项目)\s*", "", normalized).strip()
|
|
4370
|
+
normalized = re.sub(r"\s*(?:号|个|个任务|任务)\s*$", "", normalized).strip()
|
|
4371
|
+
return normalized
|
|
4372
|
+
|
|
4373
|
+
@staticmethod
|
|
4374
|
+
def _parse_chinese_numeric_reference(value: str) -> str | None:
|
|
4375
|
+
normalized = str(value or "").strip()
|
|
4376
|
+
if not normalized:
|
|
4377
|
+
return None
|
|
4378
|
+
if all(char in _CHINESE_DIGIT_MAP for char in normalized):
|
|
4379
|
+
return "".join(str(_CHINESE_DIGIT_MAP[char]) for char in normalized)
|
|
4380
|
+
if not all(char in _CHINESE_DIGIT_MAP or char in _CHINESE_UNIT_MAP for char in normalized):
|
|
4381
|
+
return None
|
|
4382
|
+
total = 0
|
|
4383
|
+
current = 0
|
|
4384
|
+
for char in normalized:
|
|
4385
|
+
if char in _CHINESE_DIGIT_MAP:
|
|
4386
|
+
current = _CHINESE_DIGIT_MAP[char]
|
|
4387
|
+
continue
|
|
4388
|
+
unit = _CHINESE_UNIT_MAP.get(char)
|
|
4389
|
+
if unit is None:
|
|
4390
|
+
return None
|
|
4391
|
+
total += (current or 1) * unit
|
|
4392
|
+
current = 0
|
|
4393
|
+
total += current
|
|
4394
|
+
return str(total) if total > 0 else None
|
|
4395
|
+
|
|
4396
|
+
def _resolve_numeric_quest_id(self, value: str | None) -> str | None:
|
|
4397
|
+
normalized = str(value or "").strip()
|
|
4398
|
+
if not normalized or not normalized.isdigit():
|
|
4399
|
+
return None
|
|
4400
|
+
if self._quest_exists(normalized):
|
|
4401
|
+
return normalized
|
|
4402
|
+
target_numeric = int(normalized)
|
|
4403
|
+
for item in self.quest_service.list_quests():
|
|
4404
|
+
quest_id = str(item.get("quest_id") or "").strip()
|
|
4405
|
+
if not quest_id.isdigit():
|
|
4406
|
+
continue
|
|
4407
|
+
if int(quest_id) == target_numeric:
|
|
4408
|
+
return quest_id
|
|
4409
|
+
return None
|
|
4410
|
+
|
|
4348
4411
|
def _quest_exists(self, quest_id: str | None) -> bool:
|
|
4349
4412
|
normalized = str(quest_id or "").strip()
|
|
4350
4413
|
if not normalized:
|
|
@@ -4352,11 +4415,22 @@ class DaemonApp:
|
|
|
4352
4415
|
return (self.home / "quests" / normalized / "quest.yaml").exists()
|
|
4353
4416
|
|
|
4354
4417
|
def _resolve_quest_reference(self, value: str | None) -> str | None:
|
|
4355
|
-
normalized =
|
|
4418
|
+
normalized = self._strip_quest_reference_noise(value)
|
|
4356
4419
|
if not normalized:
|
|
4357
4420
|
return None
|
|
4358
4421
|
if normalized in {"latest", "newest"}:
|
|
4359
4422
|
return self._latest_quest_id()
|
|
4423
|
+
literal_match = normalized if self._quest_exists(normalized) else None
|
|
4424
|
+
if literal_match:
|
|
4425
|
+
return literal_match
|
|
4426
|
+
numeric_match = self._resolve_numeric_quest_id(normalized)
|
|
4427
|
+
if numeric_match:
|
|
4428
|
+
return numeric_match
|
|
4429
|
+
chinese_numeric = self._parse_chinese_numeric_reference(normalized)
|
|
4430
|
+
if chinese_numeric:
|
|
4431
|
+
numeric_match = self._resolve_numeric_quest_id(chinese_numeric)
|
|
4432
|
+
if numeric_match:
|
|
4433
|
+
return numeric_match
|
|
4360
4434
|
return normalized
|
|
4361
4435
|
|
|
4362
4436
|
def _connector_control_reply(
|
|
@@ -4795,7 +4869,7 @@ class DaemonApp:
|
|
|
4795
4869
|
|
|
4796
4870
|
@staticmethod
|
|
4797
4871
|
def _parse_lingzhu_short_command(text: str) -> tuple[str, list[str]] | None:
|
|
4798
|
-
normalized =
|
|
4872
|
+
normalized = lingzhu_normalize_command_text(text)
|
|
4799
4873
|
if not normalized or normalized.startswith("/"):
|
|
4800
4874
|
return None
|
|
4801
4875
|
direct = _LINGZHU_SHORT_COMMAND_DIRECT_MAP.get(normalized)
|
|
@@ -4822,6 +4896,27 @@ class DaemonApp:
|
|
|
4822
4896
|
return command_name, []
|
|
4823
4897
|
return None
|
|
4824
4898
|
|
|
4899
|
+
def _lingzhu_status_hint_text(self, quest_id: str | None) -> str:
|
|
4900
|
+
if not quest_id:
|
|
4901
|
+
latest = str(self._latest_quest_id() or "").strip()
|
|
4902
|
+
bind_hint = f"绑定{latest}" if latest else "绑定最新"
|
|
4903
|
+
return f"未绑定。可说:{bind_hint}/列表/帮助"
|
|
4904
|
+
snapshot = self.quest_service.snapshot(quest_id)
|
|
4905
|
+
runtime_status = str(snapshot.get("runtime_status") or snapshot.get("status") or "").strip().lower()
|
|
4906
|
+
latest = str(self._latest_quest_id() or "").strip()
|
|
4907
|
+
bind_hint = f"绑定{latest}" if latest and latest != quest_id else "绑定最新"
|
|
4908
|
+
if runtime_status in {"running", "active"}:
|
|
4909
|
+
return f"绑{quest_id},进行中。可说:状态/总结/{bind_hint}"
|
|
4910
|
+
if runtime_status == "waiting_for_user":
|
|
4911
|
+
return f"绑{quest_id},等你确认。可说:状态/总结/{bind_hint}"
|
|
4912
|
+
if runtime_status in {"paused", "stopped"}:
|
|
4913
|
+
return f"绑{quest_id},已暂停。可说:恢复/{bind_hint}/列表"
|
|
4914
|
+
if runtime_status == "completed":
|
|
4915
|
+
return f"绑{quest_id},已完成。可说:总结/{bind_hint}/列表"
|
|
4916
|
+
if runtime_status == "error":
|
|
4917
|
+
return f"绑{quest_id},出错了。可说:状态/恢复/{bind_hint}"
|
|
4918
|
+
return f"绑{quest_id},暂无新进展。可说:状态/总结/{bind_hint}"
|
|
4919
|
+
|
|
4825
4920
|
def _lingzhu_unbound_help_text(self) -> str:
|
|
4826
4921
|
latest = str(self._latest_quest_id() or "none")
|
|
4827
4922
|
return (
|
|
@@ -5354,21 +5449,7 @@ class DaemonApp:
|
|
|
5354
5449
|
return emitted
|
|
5355
5450
|
|
|
5356
5451
|
def _lingzhu_short_status_text(self, quest_id: str | None) -> str:
|
|
5357
|
-
|
|
5358
|
-
return self._lingzhu_unbound_help_text()
|
|
5359
|
-
snapshot = self.quest_service.snapshot(quest_id)
|
|
5360
|
-
runtime_status = str(snapshot.get("runtime_status") or snapshot.get("status") or "").strip().lower()
|
|
5361
|
-
if runtime_status in {"running", "active"}:
|
|
5362
|
-
return "进行中"
|
|
5363
|
-
if runtime_status == "waiting_for_user":
|
|
5364
|
-
return "等你确认"
|
|
5365
|
-
if runtime_status in {"paused", "stopped"}:
|
|
5366
|
-
return "已暂停"
|
|
5367
|
-
if runtime_status == "completed":
|
|
5368
|
-
return "已完成"
|
|
5369
|
-
if runtime_status == "error":
|
|
5370
|
-
return "出错了"
|
|
5371
|
-
return "暂无新进展"
|
|
5452
|
+
return self._lingzhu_status_hint_text(quest_id)
|
|
5372
5453
|
|
|
5373
5454
|
@staticmethod
|
|
5374
5455
|
def _lingzhu_reply_payload(result: dict[str, Any]) -> tuple[str, str | None, str]:
|
|
@@ -11,6 +11,7 @@ from ..artifact import ArtifactService
|
|
|
11
11
|
from ..artifact.metrics import MetricContractValidationError
|
|
12
12
|
from ..bash_exec import BashExecService
|
|
13
13
|
from ..memory import MemoryService
|
|
14
|
+
from ..quest import QuestService
|
|
14
15
|
from .context import McpContext
|
|
15
16
|
|
|
16
17
|
DEFAULT_INLINE_BASH_LOG_LINE_LIMIT = 2000
|
|
@@ -18,16 +19,123 @@ DEFAULT_INLINE_BASH_LOG_HEAD_LINES = 500
|
|
|
18
19
|
DEFAULT_INLINE_BASH_LOG_TAIL_LINES = 1500
|
|
19
20
|
DEFAULT_INLINE_BASH_LOG_WINDOW_LINES = 200
|
|
20
21
|
MAX_INLINE_BASH_LOG_WINDOW_LINES = 2000
|
|
22
|
+
INTERACTION_WATCHDOG_TOOL_CALL_THRESHOLD = 25
|
|
23
|
+
INTERACTION_WATCHDOG_SILENCE_THRESHOLD_SECONDS = 30 * 60
|
|
21
24
|
LONG_BASH_LOG_HINT = (
|
|
22
25
|
"Use `bash_exec(mode='read', id=..., start=..., tail=...)` to inspect a specific log window, "
|
|
23
26
|
"or `bash_exec(mode='read', id=..., tail=...)` to inspect the latest rendered lines."
|
|
24
27
|
)
|
|
28
|
+
ARTIFACT_STATE_CHANGE_WATCHDOG_NOTES = {
|
|
29
|
+
"confirm_baseline": (
|
|
30
|
+
"Baseline confirmation changed durable quest state and this tool does not send a user-visible "
|
|
31
|
+
"summary on its own. Send one concise artifact.interact(...) update now."
|
|
32
|
+
),
|
|
33
|
+
"waive_baseline": (
|
|
34
|
+
"Baseline waiver changed durable quest state and this tool does not send a user-visible summary "
|
|
35
|
+
"on its own. Send one concise artifact.interact(...) update now."
|
|
36
|
+
),
|
|
37
|
+
"submit_paper_outline": (
|
|
38
|
+
"Paper outline state changed durably and this tool does not send a user-visible summary on its own. "
|
|
39
|
+
"Send one concise artifact.interact(...) update now."
|
|
40
|
+
),
|
|
41
|
+
"publish_baseline": (
|
|
42
|
+
"Baseline publication changed durable state and this tool does not send a user-visible summary "
|
|
43
|
+
"on its own. Send one concise artifact.interact(...) update now."
|
|
44
|
+
),
|
|
45
|
+
"attach_baseline": (
|
|
46
|
+
"Baseline attachment changed durable quest state and this tool does not send a user-visible summary "
|
|
47
|
+
"on its own. Send one concise artifact.interact(...) update now."
|
|
48
|
+
),
|
|
49
|
+
"complete_quest": (
|
|
50
|
+
"Quest completion changed durable state and this tool does not send a final user-visible summary "
|
|
51
|
+
"on its own. Send one concise artifact.interact(...) closing update now unless the user already "
|
|
52
|
+
"received an equivalent completion summary."
|
|
53
|
+
),
|
|
54
|
+
}
|
|
25
55
|
|
|
26
56
|
|
|
27
57
|
def _metric_validation_error_payload(exc: MetricContractValidationError) -> dict[str, Any]:
|
|
28
58
|
return exc.as_payload()
|
|
29
59
|
|
|
30
60
|
|
|
61
|
+
def _progress_watchdog_note(tool_call_count: int) -> str:
|
|
62
|
+
return (
|
|
63
|
+
"By the way, you have gone "
|
|
64
|
+
f"{tool_call_count} tool calls without notifying the user via artifact.interact(...). "
|
|
65
|
+
"Please report your latest progress now."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _visibility_watchdog_note(seconds_since_last_update: int) -> str:
|
|
70
|
+
minutes = max(1, seconds_since_last_update // 60)
|
|
71
|
+
return (
|
|
72
|
+
"By the way, it has been "
|
|
73
|
+
f"{minutes} minutes since the last user-visible artifact.interact(...). "
|
|
74
|
+
"Send one concise progress update now before continuing with background work."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _collect_interaction_watchdog_notes(
|
|
79
|
+
watchdog: dict[str, Any],
|
|
80
|
+
*,
|
|
81
|
+
state_change_note: str | None = None,
|
|
82
|
+
) -> list[dict[str, str]]:
|
|
83
|
+
notes: list[dict[str, str]] = []
|
|
84
|
+
count = int((watchdog or {}).get("tool_calls_since_last_artifact_interact") or 0)
|
|
85
|
+
if count >= INTERACTION_WATCHDOG_TOOL_CALL_THRESHOLD:
|
|
86
|
+
notes.append(
|
|
87
|
+
{
|
|
88
|
+
"kind": "progress",
|
|
89
|
+
"message": _progress_watchdog_note(count),
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
silence_seconds = int((watchdog or {}).get("seconds_since_last_artifact_interact") or 0)
|
|
93
|
+
if count > 0 and silence_seconds >= INTERACTION_WATCHDOG_SILENCE_THRESHOLD_SECONDS:
|
|
94
|
+
notes.append(
|
|
95
|
+
{
|
|
96
|
+
"kind": "visibility",
|
|
97
|
+
"message": _visibility_watchdog_note(silence_seconds),
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
if state_change_note:
|
|
101
|
+
notes.append(
|
|
102
|
+
{
|
|
103
|
+
"kind": "state_change",
|
|
104
|
+
"message": state_change_note,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return notes
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _attach_interaction_watchdog(
|
|
111
|
+
payload: dict[str, Any],
|
|
112
|
+
watchdog: dict[str, Any],
|
|
113
|
+
*,
|
|
114
|
+
state_change_note: str | None = None,
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
enriched = dict(payload)
|
|
117
|
+
enriched["interaction_watchdog"] = dict(watchdog or {})
|
|
118
|
+
notes = _collect_interaction_watchdog_notes(
|
|
119
|
+
watchdog,
|
|
120
|
+
state_change_note=state_change_note,
|
|
121
|
+
)
|
|
122
|
+
if not notes:
|
|
123
|
+
return enriched
|
|
124
|
+
enriched["watchdog_notes"] = notes
|
|
125
|
+
for item in notes:
|
|
126
|
+
kind = str(item.get("kind") or "").strip()
|
|
127
|
+
message = str(item.get("message") or "").strip()
|
|
128
|
+
if not message:
|
|
129
|
+
continue
|
|
130
|
+
if kind == "progress":
|
|
131
|
+
enriched["progress_watchdog_note"] = message
|
|
132
|
+
elif kind == "visibility":
|
|
133
|
+
enriched["visibility_watchdog_note"] = message
|
|
134
|
+
elif kind == "state_change":
|
|
135
|
+
enriched["state_change_watchdog_note"] = message
|
|
136
|
+
return enriched
|
|
137
|
+
|
|
138
|
+
|
|
31
139
|
def _split_bash_log_lines(log_text: str) -> list[str]:
|
|
32
140
|
return log_text.splitlines()
|
|
33
141
|
|
|
@@ -341,6 +449,7 @@ def build_memory_server(context: McpContext) -> FastMCP:
|
|
|
341
449
|
|
|
342
450
|
def build_artifact_server(context: McpContext) -> FastMCP:
|
|
343
451
|
service = ArtifactService(context.home)
|
|
452
|
+
quest_service = service.quest_service
|
|
344
453
|
server = FastMCP(
|
|
345
454
|
"artifact",
|
|
346
455
|
instructions=(
|
|
@@ -351,6 +460,19 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
351
460
|
log_level="ERROR",
|
|
352
461
|
)
|
|
353
462
|
|
|
463
|
+
def finalize_state_changing_artifact_tool(payload: dict[str, Any], *, tool_name: str) -> dict[str, Any]:
|
|
464
|
+
quest_root = context.require_quest_root().resolve()
|
|
465
|
+
quest_service.record_tool_activity(
|
|
466
|
+
quest_root,
|
|
467
|
+
tool_name=f"artifact.{tool_name}",
|
|
468
|
+
)
|
|
469
|
+
watchdog = quest_service.artifact_interaction_watchdog_status(quest_root)
|
|
470
|
+
return _attach_interaction_watchdog(
|
|
471
|
+
payload,
|
|
472
|
+
watchdog,
|
|
473
|
+
state_change_note=ARTIFACT_STATE_CHANGE_WATCHDOG_NOTES.get(tool_name),
|
|
474
|
+
)
|
|
475
|
+
|
|
354
476
|
@server.tool(name="record", description="Write a structured artifact record under the current quest.")
|
|
355
477
|
def record(payload: dict[str, Any], comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
|
|
356
478
|
enriched = dict(payload)
|
|
@@ -620,7 +742,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
620
742
|
selected_reason: str | None = None,
|
|
621
743
|
comment: str | dict[str, Any] | None = None,
|
|
622
744
|
) -> dict[str, Any]:
|
|
623
|
-
|
|
745
|
+
result = service.submit_paper_outline(
|
|
624
746
|
context.require_quest_root(),
|
|
625
747
|
mode=mode,
|
|
626
748
|
outline_id=outline_id,
|
|
@@ -632,6 +754,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
632
754
|
review_result=review_result,
|
|
633
755
|
selected_reason=selected_reason,
|
|
634
756
|
)
|
|
757
|
+
return finalize_state_changing_artifact_tool(result, tool_name="submit_paper_outline")
|
|
635
758
|
|
|
636
759
|
@server.tool(
|
|
637
760
|
name="list_paper_outlines",
|
|
@@ -733,7 +856,8 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
733
856
|
if comment is not None and "comment" not in enriched:
|
|
734
857
|
enriched["comment"] = comment
|
|
735
858
|
enriched.setdefault("source", {"kind": "artifact_publish", "quest_id": context.quest_id, "quest_root": str(context.require_quest_root())})
|
|
736
|
-
|
|
859
|
+
result = service.publish_baseline(context.require_quest_root(), enriched)
|
|
860
|
+
return finalize_state_changing_artifact_tool(result, tool_name="publish_baseline")
|
|
737
861
|
|
|
738
862
|
@server.tool(name="attach_baseline", description="Attach a published baseline to the current quest.")
|
|
739
863
|
def attach_baseline(
|
|
@@ -741,7 +865,8 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
741
865
|
variant_id: str | None = None,
|
|
742
866
|
comment: str | dict[str, Any] | None = None,
|
|
743
867
|
) -> dict[str, Any]:
|
|
744
|
-
|
|
868
|
+
result = service.attach_baseline(context.require_quest_root(), baseline_id, variant_id)
|
|
869
|
+
return finalize_state_changing_artifact_tool(result, tool_name="attach_baseline")
|
|
745
870
|
|
|
746
871
|
@server.tool(
|
|
747
872
|
name="confirm_baseline",
|
|
@@ -764,7 +889,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
764
889
|
comment: str | dict[str, Any] | None = None,
|
|
765
890
|
) -> dict[str, Any]:
|
|
766
891
|
try:
|
|
767
|
-
|
|
892
|
+
result = service.confirm_baseline(
|
|
768
893
|
context.require_quest_root(),
|
|
769
894
|
baseline_path=baseline_path,
|
|
770
895
|
comment=comment,
|
|
@@ -779,6 +904,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
779
904
|
auto_advance=auto_advance,
|
|
780
905
|
strict_metric_contract=True,
|
|
781
906
|
)
|
|
907
|
+
return finalize_state_changing_artifact_tool(result, tool_name="confirm_baseline")
|
|
782
908
|
except MetricContractValidationError as exc:
|
|
783
909
|
return _metric_validation_error_payload(exc)
|
|
784
910
|
|
|
@@ -791,12 +917,13 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
791
917
|
auto_advance: bool = True,
|
|
792
918
|
comment: str | dict[str, Any] | None = None,
|
|
793
919
|
) -> dict[str, Any]:
|
|
794
|
-
|
|
920
|
+
result = service.waive_baseline(
|
|
795
921
|
context.require_quest_root(),
|
|
796
922
|
reason=reason,
|
|
797
923
|
comment=comment,
|
|
798
924
|
auto_advance=auto_advance,
|
|
799
925
|
)
|
|
926
|
+
return finalize_state_changing_artifact_tool(result, tool_name="waive_baseline")
|
|
800
927
|
|
|
801
928
|
@server.tool(
|
|
802
929
|
name="arxiv",
|
|
@@ -852,7 +979,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
852
979
|
supersede_open_requests: bool = True,
|
|
853
980
|
comment: str | dict[str, Any] | None = None,
|
|
854
981
|
) -> dict[str, Any]:
|
|
855
|
-
|
|
982
|
+
result = service.interact(
|
|
856
983
|
context.require_quest_root(),
|
|
857
984
|
kind=kind,
|
|
858
985
|
message=message,
|
|
@@ -873,6 +1000,8 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
873
1000
|
reply_to_interaction_id=reply_to_interaction_id,
|
|
874
1001
|
supersede_open_requests=supersede_open_requests,
|
|
875
1002
|
)
|
|
1003
|
+
result["interaction_watchdog"] = quest_service.artifact_interaction_watchdog_status(context.require_quest_root())
|
|
1004
|
+
return result
|
|
876
1005
|
|
|
877
1006
|
@server.tool(
|
|
878
1007
|
name="complete_quest",
|
|
@@ -885,16 +1014,20 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
885
1014
|
summary: str = "",
|
|
886
1015
|
comment: str | dict[str, Any] | None = None,
|
|
887
1016
|
) -> dict[str, Any]:
|
|
888
|
-
|
|
1017
|
+
result = service.complete_quest(
|
|
889
1018
|
context.require_quest_root(),
|
|
890
1019
|
summary=summary,
|
|
891
1020
|
)
|
|
1021
|
+
if result.get("ok") is True and str(result.get("status") or "").strip() == "completed":
|
|
1022
|
+
return finalize_state_changing_artifact_tool(result, tool_name="complete_quest")
|
|
1023
|
+
return result
|
|
892
1024
|
|
|
893
1025
|
return server
|
|
894
1026
|
|
|
895
1027
|
|
|
896
1028
|
def build_bash_exec_server(context: McpContext) -> FastMCP:
|
|
897
1029
|
service = BashExecService(context.home)
|
|
1030
|
+
quest_service = QuestService(context.home)
|
|
898
1031
|
server = FastMCP(
|
|
899
1032
|
"bash_exec",
|
|
900
1033
|
instructions=(
|
|
@@ -944,6 +1077,15 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
|
|
|
944
1077
|
comment: str | dict[str, Any] | None = None,
|
|
945
1078
|
) -> dict[str, Any]:
|
|
946
1079
|
quest_root = context.require_quest_root().resolve()
|
|
1080
|
+
|
|
1081
|
+
def finalize(payload: dict[str, Any]) -> dict[str, Any]:
|
|
1082
|
+
quest_service.record_tool_activity(
|
|
1083
|
+
quest_root,
|
|
1084
|
+
tool_name=f"bash_exec.{normalized_mode}",
|
|
1085
|
+
)
|
|
1086
|
+
watchdog = quest_service.artifact_interaction_watchdog_status(quest_root)
|
|
1087
|
+
return _attach_interaction_watchdog(payload, watchdog)
|
|
1088
|
+
|
|
947
1089
|
normalized_mode = (mode or "detach").strip().lower()
|
|
948
1090
|
if normalized_mode == "create":
|
|
949
1091
|
normalized_mode = "await"
|
|
@@ -973,12 +1115,12 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
|
|
|
973
1115
|
"history_lines": history_lines,
|
|
974
1116
|
}
|
|
975
1117
|
if normalized_mode == "history":
|
|
976
|
-
return {
|
|
1118
|
+
return finalize({
|
|
977
1119
|
"count": len(items),
|
|
978
1120
|
"lines": history_lines,
|
|
979
1121
|
"items": items,
|
|
980
|
-
}
|
|
981
|
-
return payload
|
|
1122
|
+
})
|
|
1123
|
+
return finalize(payload)
|
|
982
1124
|
if normalized_mode == "read":
|
|
983
1125
|
bash_id = service.resolve_session_id(quest_root, id)
|
|
984
1126
|
session = service.get_session(quest_root, bash_id)
|
|
@@ -1007,7 +1149,7 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
|
|
|
1007
1149
|
tail=tail if tail is not None else tail_limit,
|
|
1008
1150
|
)
|
|
1009
1151
|
)
|
|
1010
|
-
return payload
|
|
1152
|
+
return finalize(payload)
|
|
1011
1153
|
use_tail = tail_limit is not None or before_seq is not None or after_seq is not None or normalized_order != "asc"
|
|
1012
1154
|
if use_tail:
|
|
1013
1155
|
resolved_tail_limit = max(1, min(int(tail_limit or 200), 1000))
|
|
@@ -1034,7 +1176,7 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
|
|
|
1034
1176
|
payload["after_seq"] = tail_meta.get("after_seq")
|
|
1035
1177
|
payload["before_seq"] = tail_meta.get("before_seq")
|
|
1036
1178
|
payload["order"] = normalized_order
|
|
1037
|
-
return payload
|
|
1179
|
+
return finalize(payload)
|
|
1038
1180
|
payload = service.build_tool_result(
|
|
1039
1181
|
context,
|
|
1040
1182
|
session=session,
|
|
@@ -1043,7 +1185,7 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
|
|
|
1043
1185
|
export_log_to=export_log_to,
|
|
1044
1186
|
)
|
|
1045
1187
|
payload.update(_build_default_bash_log_payload_from_path(service.terminal_log_path(quest_root, bash_id)))
|
|
1046
|
-
return payload
|
|
1188
|
+
return finalize(payload)
|
|
1047
1189
|
if normalized_mode == "kill":
|
|
1048
1190
|
bash_id = service.resolve_session_id(quest_root, id)
|
|
1049
1191
|
session = service.request_stop(
|
|
@@ -1055,17 +1197,17 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
|
|
|
1055
1197
|
)
|
|
1056
1198
|
if wait:
|
|
1057
1199
|
session = service.wait_for_session(quest_root, bash_id, timeout_seconds=timeout_seconds)
|
|
1058
|
-
return service.build_tool_result(context, session=session, include_log=False)
|
|
1200
|
+
return finalize(service.build_tool_result(context, session=session, include_log=False))
|
|
1059
1201
|
if normalized_mode == "await" and not command:
|
|
1060
1202
|
bash_id = service.resolve_session_id(quest_root, id)
|
|
1061
1203
|
session = service.wait_for_session(quest_root, bash_id, timeout_seconds=timeout_seconds)
|
|
1062
|
-
return service.build_tool_result(
|
|
1204
|
+
return finalize(service.build_tool_result(
|
|
1063
1205
|
context,
|
|
1064
1206
|
session=session,
|
|
1065
1207
|
include_log=False,
|
|
1066
1208
|
export_log=export_log,
|
|
1067
1209
|
export_log_to=export_log_to,
|
|
1068
|
-
)
|
|
1210
|
+
))
|
|
1069
1211
|
if not (command or "").strip():
|
|
1070
1212
|
raise ValueError("command is required for `detach` and `await`.")
|
|
1071
1213
|
session = service.start_session(
|
|
@@ -1078,15 +1220,15 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
|
|
|
1078
1220
|
comment=comment,
|
|
1079
1221
|
)
|
|
1080
1222
|
if normalized_mode == "detach":
|
|
1081
|
-
return service.build_tool_result(context, session=session, include_log=False)
|
|
1223
|
+
return finalize(service.build_tool_result(context, session=session, include_log=False))
|
|
1082
1224
|
session = service.wait_for_session(quest_root, str(session["bash_id"]), timeout_seconds=timeout_seconds)
|
|
1083
|
-
return service.build_tool_result(
|
|
1225
|
+
return finalize(service.build_tool_result(
|
|
1084
1226
|
context,
|
|
1085
1227
|
session=session,
|
|
1086
1228
|
include_log=False,
|
|
1087
1229
|
export_log=export_log,
|
|
1088
1230
|
export_log_to=export_log_to,
|
|
1089
|
-
)
|
|
1231
|
+
))
|
|
1090
1232
|
|
|
1091
1233
|
return server
|
|
1092
1234
|
|