indesign-cli 0.2.0__py3-none-any.whl
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.
- cli_anything/indesign/README.md +32 -0
- cli_anything/indesign/__init__.py +1 -0
- cli_anything/indesign/__main__.py +5 -0
- cli_anything/indesign/core/artifacts.py +57 -0
- cli_anything/indesign/core/catalog.py +405 -0
- cli_anything/indesign/core/domains.py +178 -0
- cli_anything/indesign/core/envelope.py +65 -0
- cli_anything/indesign/core/errors.py +30 -0
- cli_anything/indesign/core/health.py +46 -0
- cli_anything/indesign/core/hidden_backend.py +116 -0
- cli_anything/indesign/core/hidden_handler_schemas.py +223 -0
- cli_anything/indesign/core/mcp_backend.py +152 -0
- cli_anything/indesign/core/node_setup.py +35 -0
- cli_anything/indesign/core/paths.py +41 -0
- cli_anything/indesign/core/plugins/__init__.py +2 -0
- cli_anything/indesign/core/plugins/backend.py +90 -0
- cli_anything/indesign/core/plugins/discovery.py +69 -0
- cli_anything/indesign/core/plugins/host_actions.py +76 -0
- cli_anything/indesign/core/plugins/install.py +38 -0
- cli_anything/indesign/core/plugins/manifest.py +279 -0
- cli_anything/indesign/core/plugins/validate.py +181 -0
- cli_anything/indesign/core/router.py +217 -0
- cli_anything/indesign/core/runtime.py +59 -0
- cli_anything/indesign/core/scripts.py +44 -0
- cli_anything/indesign/core/session.py +68 -0
- cli_anything/indesign/indesign_cli.py +320 -0
- cli_anything/indesign/node/hidden_handler_bridge.mjs +111 -0
- cli_anything/indesign/server/package-lock.json +168 -0
- cli_anything/indesign/server/package.json +45 -0
- cli_anything/indesign/server/src/advanced/index.js +76 -0
- cli_anything/indesign/server/src/core/InDesignMCPServer.js +273 -0
- cli_anything/indesign/server/src/core/scriptExecutor.js +271 -0
- cli_anything/indesign/server/src/core/sessionManager.js +545 -0
- cli_anything/indesign/server/src/handlers/advancedTemplateHandlers.js +1072 -0
- cli_anything/indesign/server/src/handlers/bookHandlers.js +490 -0
- cli_anything/indesign/server/src/handlers/documentHandlers.js +1472 -0
- cli_anything/indesign/server/src/handlers/exportHandlers.js +208 -0
- cli_anything/indesign/server/src/handlers/graphicsHandlers.js +605 -0
- cli_anything/indesign/server/src/handlers/groupHandlers.js +358 -0
- cli_anything/indesign/server/src/handlers/helpHandlers.js +347 -0
- cli_anything/indesign/server/src/handlers/index.js +77 -0
- cli_anything/indesign/server/src/handlers/layerHandlers.js +75 -0
- cli_anything/indesign/server/src/handlers/masterSpreadHandlers.js +451 -0
- cli_anything/indesign/server/src/handlers/pageHandlers.js +698 -0
- cli_anything/indesign/server/src/handlers/pageItemHandlers.js +704 -0
- cli_anything/indesign/server/src/handlers/presentationHandlers.js +220 -0
- cli_anything/indesign/server/src/handlers/spreadHandlers.js +348 -0
- cli_anything/indesign/server/src/handlers/styleHandlers.js +458 -0
- cli_anything/indesign/server/src/handlers/textHandlers.js +431 -0
- cli_anything/indesign/server/src/handlers/utilityHandlers.js +83 -0
- cli_anything/indesign/server/src/index.js +17 -0
- cli_anything/indesign/server/src/types/index.js +106 -0
- cli_anything/indesign/server/src/types/toolDefinitionsAdvancedTemplates.js +144 -0
- cli_anything/indesign/server/src/types/toolDefinitionsBook.js +224 -0
- cli_anything/indesign/server/src/types/toolDefinitionsContent.js +353 -0
- cli_anything/indesign/server/src/types/toolDefinitionsDocument.js +409 -0
- cli_anything/indesign/server/src/types/toolDefinitionsExport.js +65 -0
- cli_anything/indesign/server/src/types/toolDefinitionsLayer.js +40 -0
- cli_anything/indesign/server/src/types/toolDefinitionsMasterSpread.js +160 -0
- cli_anything/indesign/server/src/types/toolDefinitionsPage.js +271 -0
- cli_anything/indesign/server/src/types/toolDefinitionsPageItemGroup.js +437 -0
- cli_anything/indesign/server/src/types/toolDefinitionsPresentation.js +83 -0
- cli_anything/indesign/server/src/types/toolDefinitionsSpread.js +158 -0
- cli_anything/indesign/server/src/types/toolDefinitionsUtility.js +40 -0
- cli_anything/indesign/server/src/utils/stringUtils.js +107 -0
- cli_anything/indesign/skills/SKILL.md +198 -0
- indesign_cli-0.2.0.dist-info/METADATA +267 -0
- indesign_cli-0.2.0.dist-info/RECORD +72 -0
- indesign_cli-0.2.0.dist-info/WHEEL +5 -0
- indesign_cli-0.2.0.dist-info/entry_points.txt +3 -0
- indesign_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- indesign_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .errors import CliError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def request_id() -> str:
|
|
11
|
+
return uuid.uuid4().hex[:16]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def success(
|
|
15
|
+
*,
|
|
16
|
+
command: str,
|
|
17
|
+
data: Any,
|
|
18
|
+
duration_ms: int,
|
|
19
|
+
tool_id: str | None = None,
|
|
20
|
+
domain: str | None = None,
|
|
21
|
+
source: str | None = None,
|
|
22
|
+
backend: str | None = None,
|
|
23
|
+
warnings: list[str] | None = None,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
return {
|
|
26
|
+
"schema_version": 1,
|
|
27
|
+
"ok": True,
|
|
28
|
+
"exit_code": 0,
|
|
29
|
+
"request_id": request_id(),
|
|
30
|
+
"command": command,
|
|
31
|
+
"tool_id": tool_id,
|
|
32
|
+
"domain": domain,
|
|
33
|
+
"source": source,
|
|
34
|
+
"backend": backend,
|
|
35
|
+
"mcp_ok": True,
|
|
36
|
+
"tool_success": True,
|
|
37
|
+
"raw_result_type": "json",
|
|
38
|
+
"duration_ms": duration_ms,
|
|
39
|
+
"data": data,
|
|
40
|
+
"warnings": warnings or [],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def failure(*, command: str, error: CliError, duration_ms: int) -> dict[str, Any]:
|
|
45
|
+
return {
|
|
46
|
+
"schema_version": 1,
|
|
47
|
+
"ok": False,
|
|
48
|
+
"exit_code": 1,
|
|
49
|
+
"request_id": request_id(),
|
|
50
|
+
"command": command,
|
|
51
|
+
"duration_ms": duration_ms,
|
|
52
|
+
"error": {
|
|
53
|
+
"type": error.__class__.__name__,
|
|
54
|
+
"code": error.code,
|
|
55
|
+
"message": error.message,
|
|
56
|
+
"details": error.details,
|
|
57
|
+
"retryable": error.retryable,
|
|
58
|
+
"hint": error.hint,
|
|
59
|
+
},
|
|
60
|
+
"warnings": [],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def now_ms() -> int:
|
|
65
|
+
return int(time.perf_counter() * 1000)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CliError(Exception):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
message: str,
|
|
8
|
+
*,
|
|
9
|
+
code: str = "CLI_ERROR",
|
|
10
|
+
retryable: bool = False,
|
|
11
|
+
details: dict | None = None,
|
|
12
|
+
hint: str | None = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.message = message
|
|
16
|
+
self.code = code
|
|
17
|
+
self.retryable = retryable
|
|
18
|
+
self.details = details or {}
|
|
19
|
+
self.hint = hint
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TimeoutError(CliError):
|
|
23
|
+
def __init__(self, message: str, *, details: dict | None = None) -> None:
|
|
24
|
+
super().__init__(
|
|
25
|
+
message,
|
|
26
|
+
code="TIMEOUT",
|
|
27
|
+
retryable=True,
|
|
28
|
+
details=details,
|
|
29
|
+
hint="缩短脚本或增加 --timeout;若 InDesign 卡住,先检查应用窗口状态。",
|
|
30
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def health(repo_root: Path, deep: bool = False) -> dict[str, Any]:
|
|
10
|
+
payload: dict[str, Any] = {
|
|
11
|
+
"deep": deep,
|
|
12
|
+
"node": {"available": shutil.which("node") is not None},
|
|
13
|
+
"python": {"available": shutil.which("python") is not None},
|
|
14
|
+
"node_entry_advanced": {
|
|
15
|
+
"path": "src/advanced/index.js",
|
|
16
|
+
"exists": (repo_root / "src/advanced/index.js").exists(),
|
|
17
|
+
},
|
|
18
|
+
"node_entry_classic": {
|
|
19
|
+
"path": "src/index.js",
|
|
20
|
+
"exists": (repo_root / "src/index.js").exists(),
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
if deep:
|
|
24
|
+
payload["winax"] = _check_winax(repo_root)
|
|
25
|
+
payload["indesign_com"] = {
|
|
26
|
+
"checked": False,
|
|
27
|
+
"available": None,
|
|
28
|
+
"reason": "health --deep 不主动连接 COM,避免隐式启动或干扰 InDesign;真实验证请运行 INDESIGN_E2E=1 的 E2E 测试。",
|
|
29
|
+
}
|
|
30
|
+
return payload
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check_winax(repo_root: Path) -> dict[str, Any]:
|
|
34
|
+
try:
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
["node", "-e", "require('winax'); process.stdout.write('ok')"],
|
|
37
|
+
cwd=repo_root,
|
|
38
|
+
text=True,
|
|
39
|
+
stdout=subprocess.PIPE,
|
|
40
|
+
stderr=subprocess.PIPE,
|
|
41
|
+
timeout=10,
|
|
42
|
+
check=False,
|
|
43
|
+
)
|
|
44
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
45
|
+
return {"checked": True, "available": False}
|
|
46
|
+
return {"checked": True, "available": result.returncode == 0}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .errors import CliError, TimeoutError
|
|
10
|
+
from .hidden_handler_schemas import HIDDEN_HANDLER_SCHEMAS
|
|
11
|
+
from .paths import scrub_text_paths
|
|
12
|
+
from .runtime import hidden_handler_bridge_path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HiddenHandlerBackend:
|
|
16
|
+
def __init__(self, repo_root: Path, timeout_seconds: int = 60) -> None:
|
|
17
|
+
self.repo_root = repo_root
|
|
18
|
+
self.timeout_seconds = timeout_seconds
|
|
19
|
+
|
|
20
|
+
def schema(self, tool_id: str) -> dict[str, Any]:
|
|
21
|
+
try:
|
|
22
|
+
return HIDDEN_HANDLER_SCHEMAS[tool_id]
|
|
23
|
+
except KeyError as exc:
|
|
24
|
+
raise CliError(f"Hidden handler schema missing: {tool_id}", code="SCHEMA_NOT_FOUND") from exc
|
|
25
|
+
|
|
26
|
+
def call_tool(self, tool: dict[str, Any], arguments: dict[str, Any]) -> dict[str, Any]:
|
|
27
|
+
schema = self.schema(tool["id"])
|
|
28
|
+
self._validate_required(schema, arguments)
|
|
29
|
+
|
|
30
|
+
bridge = hidden_handler_bridge_path()
|
|
31
|
+
|
|
32
|
+
request = {
|
|
33
|
+
"domain": tool["domain"],
|
|
34
|
+
"name": tool["name"],
|
|
35
|
+
"args": arguments,
|
|
36
|
+
}
|
|
37
|
+
try:
|
|
38
|
+
proc = subprocess.run(
|
|
39
|
+
["node", str(bridge)],
|
|
40
|
+
cwd=self.repo_root,
|
|
41
|
+
env={**os.environ, "INDESIGN_CLI_SERVER_ROOT": str(self.repo_root)},
|
|
42
|
+
input=json.dumps(request, ensure_ascii=False),
|
|
43
|
+
text=True,
|
|
44
|
+
encoding="utf-8",
|
|
45
|
+
stdout=subprocess.PIPE,
|
|
46
|
+
stderr=subprocess.PIPE,
|
|
47
|
+
check=False,
|
|
48
|
+
timeout=self.timeout_seconds,
|
|
49
|
+
)
|
|
50
|
+
except subprocess.TimeoutExpired as exc:
|
|
51
|
+
raise TimeoutError(
|
|
52
|
+
"Hidden handler bridge timed out",
|
|
53
|
+
details={"tool": tool["id"], "stderr_tail": scrub_text_paths((exc.stderr or "")[-2000:])},
|
|
54
|
+
) from exc
|
|
55
|
+
except OSError as exc:
|
|
56
|
+
raise CliError("Failed to start hidden handler bridge", code="HIDDEN_HANDLER_START_FAILED") from exc
|
|
57
|
+
|
|
58
|
+
payload = self._parse_bridge_payload(proc, tool["id"])
|
|
59
|
+
if not payload.get("ok"):
|
|
60
|
+
error = payload.get("error", {})
|
|
61
|
+
raise CliError(
|
|
62
|
+
"Hidden handler bridge failed",
|
|
63
|
+
code=error.get("code", "HIDDEN_HANDLER_FAILED"),
|
|
64
|
+
details={
|
|
65
|
+
"tool": tool["id"],
|
|
66
|
+
"message": scrub_text_paths(str(error.get("message", ""))),
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
result = payload.get("result", {})
|
|
71
|
+
if isinstance(result, dict) and result.get("success") is False:
|
|
72
|
+
raise CliError(
|
|
73
|
+
"Hidden handler failed",
|
|
74
|
+
code="HIDDEN_HANDLER_FAILED",
|
|
75
|
+
details={"tool": tool["id"], "operation": result.get("operation")},
|
|
76
|
+
)
|
|
77
|
+
if isinstance(result, dict) and isinstance(result.get("result"), str) and result["result"].startswith("Error:"):
|
|
78
|
+
raise CliError(
|
|
79
|
+
"Hidden handler failed",
|
|
80
|
+
code="HIDDEN_HANDLER_FAILED",
|
|
81
|
+
details={
|
|
82
|
+
"tool": tool["id"],
|
|
83
|
+
"operation": result.get("operation"),
|
|
84
|
+
"result": scrub_text_paths(result["result"]),
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
def _parse_bridge_payload(self, proc: subprocess.CompletedProcess[str], tool_id: str) -> dict[str, Any]:
|
|
90
|
+
stderr_tail = scrub_text_paths((proc.stderr or "")[-2000:])
|
|
91
|
+
if proc.returncode != 0:
|
|
92
|
+
raise CliError(
|
|
93
|
+
"Hidden handler bridge exited with an error",
|
|
94
|
+
code="HIDDEN_HANDLER_BRIDGE_FAILED",
|
|
95
|
+
details={"tool": tool_id, "returncode": proc.returncode, "stderr_tail": stderr_tail},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
stdout = (proc.stdout or "").strip()
|
|
99
|
+
try:
|
|
100
|
+
payload = json.loads(stdout)
|
|
101
|
+
except json.JSONDecodeError as exc:
|
|
102
|
+
raise CliError(
|
|
103
|
+
"Hidden handler bridge response is not JSON",
|
|
104
|
+
code="HIDDEN_HANDLER_BAD_JSON",
|
|
105
|
+
details={"tool": tool_id, "stdout": scrub_text_paths(stdout[-1000:]), "stderr_tail": stderr_tail},
|
|
106
|
+
) from exc
|
|
107
|
+
if not isinstance(payload, dict):
|
|
108
|
+
raise CliError("Hidden handler bridge response must be an object", code="HIDDEN_HANDLER_BAD_JSON")
|
|
109
|
+
return payload
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _validate_required(schema: dict[str, Any], arguments: dict[str, Any]) -> None:
|
|
113
|
+
for key in schema.get("required", []):
|
|
114
|
+
value = arguments.get(key)
|
|
115
|
+
if value in (None, ""):
|
|
116
|
+
raise CliError(f"Missing required argument: {key}", code="MISSING_ARGUMENT", details={"argument": key})
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _object_schema(properties: dict[str, dict[str, Any]], required: list[str] | None = None) -> dict[str, Any]:
|
|
7
|
+
schema: dict[str, Any] = {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": False,
|
|
10
|
+
"properties": properties,
|
|
11
|
+
}
|
|
12
|
+
if required:
|
|
13
|
+
schema["required"] = required
|
|
14
|
+
return schema
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _string(description: str) -> dict[str, Any]:
|
|
18
|
+
return {"type": "string", "description": description}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _number(description: str, *, minimum: int | float | None = None) -> dict[str, Any]:
|
|
22
|
+
schema: dict[str, Any] = {"type": "number", "description": description}
|
|
23
|
+
if minimum is not None:
|
|
24
|
+
schema["minimum"] = minimum
|
|
25
|
+
return schema
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _boolean(description: str, default: bool | None = None) -> dict[str, Any]:
|
|
29
|
+
schema: dict[str, Any] = {"type": "boolean", "description": description}
|
|
30
|
+
if default is not None:
|
|
31
|
+
schema["default"] = default
|
|
32
|
+
return schema
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
BOOK_PATH = _string("InDesign Book 文件路径,通常为 .indb")
|
|
36
|
+
DOCUMENT_PATH = _string("要加入 Book 的 InDesign 文档路径,通常为 .indd")
|
|
37
|
+
OUTPUT_PATH = _string("输出文件或输出目录路径")
|
|
38
|
+
|
|
39
|
+
BOOK_SYNC_PROPERTIES = {
|
|
40
|
+
"automaticPagination": _boolean("是否启用自动页码"),
|
|
41
|
+
"automaticDocumentConversion": _boolean("是否启用自动文档转换"),
|
|
42
|
+
"insertBlankPage": _boolean("是否插入空白页"),
|
|
43
|
+
"mergeIdenticalLayers": _boolean("是否合并同名图层"),
|
|
44
|
+
"synchronizeBulletNumberingList": _boolean("是否同步项目符号和编号列表"),
|
|
45
|
+
"synchronizeCellStyle": _boolean("是否同步单元格样式"),
|
|
46
|
+
"synchronizeCharacterStyle": _boolean("是否同步字符样式"),
|
|
47
|
+
"synchronizeConditionalText": _boolean("是否同步条件文本"),
|
|
48
|
+
"synchronizeCrossReferenceFormat": _boolean("是否同步交叉引用格式"),
|
|
49
|
+
"synchronizeMasterPage": _boolean("是否同步主页"),
|
|
50
|
+
"synchronizeObjectStyle": _boolean("是否同步对象样式"),
|
|
51
|
+
"synchronizeParagraphStyle": _boolean("是否同步段落样式"),
|
|
52
|
+
"synchronizeSwatch": _boolean("是否同步色板"),
|
|
53
|
+
"synchronizeTableOfContentStyle": _boolean("是否同步目录样式"),
|
|
54
|
+
"synchronizeTableStyle": _boolean("是否同步表格样式"),
|
|
55
|
+
"synchronizeTextVariable": _boolean("是否同步文本变量"),
|
|
56
|
+
"synchronizeTrapStyle": _boolean("是否同步陷印样式"),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
HIDDEN_HANDLER_SCHEMAS: dict[str, dict[str, Any]] = {
|
|
60
|
+
"book.create_book": _object_schema(
|
|
61
|
+
{"filePath": BOOK_PATH},
|
|
62
|
+
["filePath"],
|
|
63
|
+
),
|
|
64
|
+
"book.open_book": _object_schema(
|
|
65
|
+
{"filePath": BOOK_PATH},
|
|
66
|
+
["filePath"],
|
|
67
|
+
),
|
|
68
|
+
"book.add_document_to_book": _object_schema(
|
|
69
|
+
{
|
|
70
|
+
"bookPath": BOOK_PATH,
|
|
71
|
+
"documentPath": DOCUMENT_PATH,
|
|
72
|
+
},
|
|
73
|
+
["bookPath", "documentPath"],
|
|
74
|
+
),
|
|
75
|
+
"book.synchronize_book": _object_schema(
|
|
76
|
+
{"bookPath": BOOK_PATH},
|
|
77
|
+
["bookPath"],
|
|
78
|
+
),
|
|
79
|
+
"book.export_book": _object_schema(
|
|
80
|
+
{
|
|
81
|
+
"bookPath": BOOK_PATH,
|
|
82
|
+
"outputPath": OUTPUT_PATH,
|
|
83
|
+
"format": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"enum": ["PDF", "EPUB", "HTML"],
|
|
86
|
+
"default": "PDF",
|
|
87
|
+
"description": "导出格式",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
["bookPath", "outputPath"],
|
|
91
|
+
),
|
|
92
|
+
"book.package_book": _object_schema(
|
|
93
|
+
{
|
|
94
|
+
"bookPath": BOOK_PATH,
|
|
95
|
+
"outputPath": OUTPUT_PATH,
|
|
96
|
+
"copyingFonts": _boolean("是否复制字体", True),
|
|
97
|
+
"copyingLinkedGraphics": _boolean("是否复制链接图像", True),
|
|
98
|
+
"copyingProfiles": _boolean("是否复制色彩配置文件", True),
|
|
99
|
+
"updatingGraphics": _boolean("是否更新图像链接", True),
|
|
100
|
+
"includingHiddenLayers": _boolean("是否包含隐藏图层", False),
|
|
101
|
+
"ignorePreflightErrors": _boolean("是否忽略预检错误", False),
|
|
102
|
+
"creatingReport": _boolean("是否创建打包报告", True),
|
|
103
|
+
"includeIdml": _boolean("是否包含 IDML", False),
|
|
104
|
+
"includePdf": _boolean("是否包含 PDF", False),
|
|
105
|
+
},
|
|
106
|
+
["bookPath", "outputPath"],
|
|
107
|
+
),
|
|
108
|
+
"book.get_book_info": _object_schema(
|
|
109
|
+
{"bookPath": BOOK_PATH},
|
|
110
|
+
["bookPath"],
|
|
111
|
+
),
|
|
112
|
+
"book.list_books": _object_schema({}),
|
|
113
|
+
"book.repaginate_book": _object_schema(
|
|
114
|
+
{"bookPath": BOOK_PATH},
|
|
115
|
+
["bookPath"],
|
|
116
|
+
),
|
|
117
|
+
"book.update_all_cross_references": _object_schema(
|
|
118
|
+
{"bookPath": BOOK_PATH},
|
|
119
|
+
["bookPath"],
|
|
120
|
+
),
|
|
121
|
+
"book.update_all_numbers": _object_schema(
|
|
122
|
+
{"bookPath": BOOK_PATH},
|
|
123
|
+
["bookPath"],
|
|
124
|
+
),
|
|
125
|
+
"book.update_chapter_and_paragraph_numbers": _object_schema(
|
|
126
|
+
{"bookPath": BOOK_PATH},
|
|
127
|
+
["bookPath"],
|
|
128
|
+
),
|
|
129
|
+
"book.preflight_book": _object_schema(
|
|
130
|
+
{
|
|
131
|
+
"bookPath": BOOK_PATH,
|
|
132
|
+
"outputPath": OUTPUT_PATH,
|
|
133
|
+
"autoOpen": _boolean("生成预检报告后是否自动打开", False),
|
|
134
|
+
},
|
|
135
|
+
["bookPath"],
|
|
136
|
+
),
|
|
137
|
+
"book.print_book": _object_schema(
|
|
138
|
+
{
|
|
139
|
+
"bookPath": BOOK_PATH,
|
|
140
|
+
"printDialog": _boolean("是否显示打印对话框", True),
|
|
141
|
+
"printerPreset": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"default": "DEFAULT_VALUE",
|
|
144
|
+
"description": "InDesign PrinterPresetTypes 枚举名",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
["bookPath"],
|
|
148
|
+
),
|
|
149
|
+
"book.set_book_properties": _object_schema(
|
|
150
|
+
{"bookPath": BOOK_PATH, **BOOK_SYNC_PROPERTIES},
|
|
151
|
+
["bookPath"],
|
|
152
|
+
),
|
|
153
|
+
"presentation.create_presentation_document": _object_schema(
|
|
154
|
+
{
|
|
155
|
+
"preset": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"enum": ["A3_LANDSCAPE", "A4_LANDSCAPE", "RATIO_16x9"],
|
|
158
|
+
"default": "A3_LANDSCAPE",
|
|
159
|
+
"description": "演示文稿页面尺寸预设",
|
|
160
|
+
},
|
|
161
|
+
"width": _number("自定义页面宽度,单位 mm", minimum=1),
|
|
162
|
+
"height": _number("自定义页面高度,单位 mm", minimum=1),
|
|
163
|
+
"pages": {
|
|
164
|
+
"type": "integer",
|
|
165
|
+
"minimum": 1,
|
|
166
|
+
"default": 1,
|
|
167
|
+
"description": "初始页数",
|
|
168
|
+
},
|
|
169
|
+
"facingPages": _boolean("是否启用对页", False),
|
|
170
|
+
},
|
|
171
|
+
),
|
|
172
|
+
"presentation.add_cover_page": _object_schema(
|
|
173
|
+
{
|
|
174
|
+
"title": _string("封面标题"),
|
|
175
|
+
"subtitle": _string("封面副标题"),
|
|
176
|
+
"bgImagePath": _string("封面背景图片路径"),
|
|
177
|
+
},
|
|
178
|
+
),
|
|
179
|
+
"presentation.add_section_page": _object_schema(
|
|
180
|
+
{"title": _string("章节页标题")},
|
|
181
|
+
),
|
|
182
|
+
"presentation.add_full_bleed_image": _object_schema(
|
|
183
|
+
{
|
|
184
|
+
"filePath": _string("要铺满当前页的图片路径"),
|
|
185
|
+
"caption": _string("可选图片说明"),
|
|
186
|
+
},
|
|
187
|
+
["filePath"],
|
|
188
|
+
),
|
|
189
|
+
"presentation.add_image_grid": _object_schema(
|
|
190
|
+
{
|
|
191
|
+
"files": {
|
|
192
|
+
"type": "array",
|
|
193
|
+
"items": {"type": "string"},
|
|
194
|
+
"description": "要置入网格的图片路径列表",
|
|
195
|
+
},
|
|
196
|
+
"rows": {
|
|
197
|
+
"type": "integer",
|
|
198
|
+
"minimum": 1,
|
|
199
|
+
"default": 2,
|
|
200
|
+
"description": "网格行数",
|
|
201
|
+
},
|
|
202
|
+
"columns": {
|
|
203
|
+
"type": "integer",
|
|
204
|
+
"minimum": 1,
|
|
205
|
+
"default": 3,
|
|
206
|
+
"description": "网格列数",
|
|
207
|
+
},
|
|
208
|
+
"gap": _number("网格间距,单位与文档标尺一致", minimum=0),
|
|
209
|
+
},
|
|
210
|
+
["files"],
|
|
211
|
+
),
|
|
212
|
+
"presentation.export_presentation_pdf": _object_schema(
|
|
213
|
+
{
|
|
214
|
+
"filePath": _string("PDF 输出路径"),
|
|
215
|
+
"preset": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"default": "High Quality Print",
|
|
218
|
+
"description": "InDesign PDF 导出预设名称",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
["filePath"],
|
|
222
|
+
),
|
|
223
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import threading
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from .errors import CliError, TimeoutError
|
|
10
|
+
from .paths import scrub_text_paths
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class McpBackend:
|
|
14
|
+
def __init__(self, repo_root: Path, entry: str, timeout_seconds: int = 30) -> None:
|
|
15
|
+
self.repo_root = repo_root
|
|
16
|
+
self.entry = entry
|
|
17
|
+
self.timeout_seconds = timeout_seconds
|
|
18
|
+
self._next_id = 1
|
|
19
|
+
|
|
20
|
+
def list_tools(self) -> list[dict[str, Any]]:
|
|
21
|
+
response = self._with_process(lambda proc: self._request(proc, "tools/list", {}))
|
|
22
|
+
return response.get("tools", [])
|
|
23
|
+
|
|
24
|
+
def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
25
|
+
response = self._with_process(
|
|
26
|
+
lambda proc: self._request(proc, "tools/call", {"name": name, "arguments": arguments})
|
|
27
|
+
)
|
|
28
|
+
return self._parse_tool_response(name, response)
|
|
29
|
+
|
|
30
|
+
def _with_process(self, action: Callable[[subprocess.Popen[str]], dict[str, Any]]) -> dict[str, Any]:
|
|
31
|
+
entry_path = self.repo_root / self.entry
|
|
32
|
+
if not entry_path.exists():
|
|
33
|
+
raise CliError(f"MCP entry not found: {self.entry}", code="MCP_ENTRY_NOT_FOUND")
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
proc = subprocess.Popen(
|
|
37
|
+
["node", self.entry],
|
|
38
|
+
cwd=self.repo_root,
|
|
39
|
+
stdin=subprocess.PIPE,
|
|
40
|
+
stdout=subprocess.PIPE,
|
|
41
|
+
stderr=subprocess.PIPE,
|
|
42
|
+
text=True,
|
|
43
|
+
encoding="utf-8",
|
|
44
|
+
)
|
|
45
|
+
except OSError as exc:
|
|
46
|
+
raise CliError("Failed to start MCP process", code="MCP_START_FAILED", details={"entry": self.entry}) from exc
|
|
47
|
+
|
|
48
|
+
timed_out = {"value": False}
|
|
49
|
+
stderr_tail: list[str] = []
|
|
50
|
+
|
|
51
|
+
def drain_stderr() -> None:
|
|
52
|
+
if proc.stderr is None:
|
|
53
|
+
return
|
|
54
|
+
for line in proc.stderr:
|
|
55
|
+
stderr_tail.append(scrub_text_paths(line.strip()))
|
|
56
|
+
del stderr_tail[:-20]
|
|
57
|
+
|
|
58
|
+
def kill_process() -> None:
|
|
59
|
+
timed_out["value"] = True
|
|
60
|
+
proc.kill()
|
|
61
|
+
|
|
62
|
+
timer = threading.Timer(self.timeout_seconds, kill_process)
|
|
63
|
+
stderr_thread = threading.Thread(target=drain_stderr, daemon=True)
|
|
64
|
+
try:
|
|
65
|
+
timer.start()
|
|
66
|
+
stderr_thread.start()
|
|
67
|
+
self._request(
|
|
68
|
+
proc,
|
|
69
|
+
"initialize",
|
|
70
|
+
{
|
|
71
|
+
"protocolVersion": "2024-11-05",
|
|
72
|
+
"capabilities": {},
|
|
73
|
+
"clientInfo": {"name": "indesign-cli", "version": "0.2.0"},
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
self._notify(proc, "notifications/initialized", {})
|
|
77
|
+
return action(proc)
|
|
78
|
+
finally:
|
|
79
|
+
timer.cancel()
|
|
80
|
+
if proc.poll() is None:
|
|
81
|
+
proc.terminate()
|
|
82
|
+
try:
|
|
83
|
+
proc.wait(timeout=3)
|
|
84
|
+
except subprocess.TimeoutExpired:
|
|
85
|
+
proc.kill()
|
|
86
|
+
if timed_out["value"]:
|
|
87
|
+
raise TimeoutError("MCP process timed out", details={"entry": self.entry, "stderr_tail": stderr_tail})
|
|
88
|
+
|
|
89
|
+
def _request(self, proc: subprocess.Popen[str], method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
90
|
+
if proc.stdin is None or proc.stdout is None:
|
|
91
|
+
raise CliError("MCP process stdio is unavailable", code="MCP_STDIO_UNAVAILABLE")
|
|
92
|
+
request = {"jsonrpc": "2.0", "id": self._next_id, "method": method, "params": params}
|
|
93
|
+
self._next_id += 1
|
|
94
|
+
proc.stdin.write(json.dumps(request, ensure_ascii=False) + "\n")
|
|
95
|
+
proc.stdin.flush()
|
|
96
|
+
line = proc.stdout.readline()
|
|
97
|
+
if line == "":
|
|
98
|
+
raise TimeoutError("MCP process ended before response", details={"method": method, "entry": self.entry})
|
|
99
|
+
try:
|
|
100
|
+
response = json.loads(line)
|
|
101
|
+
except json.JSONDecodeError as exc:
|
|
102
|
+
raise CliError("MCP response is not JSON", code="MCP_BAD_JSON", details={"line": line[:500]}) from exc
|
|
103
|
+
if "error" in response:
|
|
104
|
+
raise CliError(
|
|
105
|
+
response["error"].get("message", "MCP request failed"),
|
|
106
|
+
code="MCP_PROTOCOL_ERROR",
|
|
107
|
+
retryable=False,
|
|
108
|
+
details=response["error"],
|
|
109
|
+
)
|
|
110
|
+
return response.get("result", {})
|
|
111
|
+
|
|
112
|
+
def _notify(self, proc: subprocess.Popen[str], method: str, params: dict[str, Any]) -> None:
|
|
113
|
+
if proc.stdin is None:
|
|
114
|
+
raise CliError("MCP process stdin is unavailable", code="MCP_STDIN_UNAVAILABLE")
|
|
115
|
+
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "method": method, "params": params}) + "\n")
|
|
116
|
+
proc.stdin.flush()
|
|
117
|
+
|
|
118
|
+
def _parse_tool_response(self, name: str, response: dict[str, Any]) -> dict[str, Any]:
|
|
119
|
+
content = response.get("content", [])
|
|
120
|
+
if not content:
|
|
121
|
+
return response
|
|
122
|
+
|
|
123
|
+
first = content[0]
|
|
124
|
+
if first.get("type") != "text":
|
|
125
|
+
return response
|
|
126
|
+
|
|
127
|
+
text = first.get("text", "")
|
|
128
|
+
try:
|
|
129
|
+
parsed = json.loads(text)
|
|
130
|
+
except json.JSONDecodeError:
|
|
131
|
+
if response.get("isError"):
|
|
132
|
+
raise CliError(
|
|
133
|
+
"MCP tool failed",
|
|
134
|
+
code="MCP_TOOL_FAILED",
|
|
135
|
+
details={"tool": name, "result": scrub_text_paths(text)},
|
|
136
|
+
)
|
|
137
|
+
return response
|
|
138
|
+
|
|
139
|
+
if response.get("isError") or (isinstance(parsed, dict) and parsed.get("success") is False):
|
|
140
|
+
details = {"tool": name}
|
|
141
|
+
if isinstance(parsed, dict):
|
|
142
|
+
details["operation"] = parsed.get("operation")
|
|
143
|
+
details["result"] = scrub_text_paths(str(parsed.get("result", "")))
|
|
144
|
+
raise CliError("MCP tool failed", code="MCP_TOOL_FAILED", details=details)
|
|
145
|
+
|
|
146
|
+
payload = {"content": content, "parsed": parsed}
|
|
147
|
+
if isinstance(parsed, dict) and isinstance(parsed.get("result"), str):
|
|
148
|
+
try:
|
|
149
|
+
payload["result_json"] = json.loads(parsed["result"])
|
|
150
|
+
except json.JSONDecodeError:
|
|
151
|
+
pass
|
|
152
|
+
return payload
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .errors import CliError
|
|
8
|
+
from .paths import scrub_text_paths
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_node_dependencies(server_root: Path) -> dict[str, Any]:
|
|
12
|
+
try:
|
|
13
|
+
result = subprocess.run(
|
|
14
|
+
["npm", "install"],
|
|
15
|
+
cwd=server_root,
|
|
16
|
+
text=True,
|
|
17
|
+
encoding="utf-8",
|
|
18
|
+
stdout=subprocess.PIPE,
|
|
19
|
+
stderr=subprocess.PIPE,
|
|
20
|
+
check=False,
|
|
21
|
+
timeout=180,
|
|
22
|
+
)
|
|
23
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
24
|
+
raise CliError("Failed to install Node dependencies", code="NPM_INSTALL_FAILED") from exc
|
|
25
|
+
|
|
26
|
+
payload = {
|
|
27
|
+
"ok": result.returncode == 0,
|
|
28
|
+
"server_root": str(server_root),
|
|
29
|
+
"returncode": result.returncode,
|
|
30
|
+
"stdout_tail": scrub_text_paths((result.stdout or "")[-2000:]),
|
|
31
|
+
"stderr_tail": scrub_text_paths((result.stderr or "")[-2000:]),
|
|
32
|
+
}
|
|
33
|
+
if result.returncode != 0:
|
|
34
|
+
raise CliError("npm install failed", code="NPM_INSTALL_FAILED", details=payload)
|
|
35
|
+
return payload
|