From 7e33e398ce0213490e1975532c520e87768def8f Mon Sep 17 00:00:00 2001 From: tanjianbin <632190820@qq.com> Date: Fri, 8 May 2026 17:15:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(k3cloud):=20=E6=96=B0=E5=A2=9E=E5=8D=95?= =?UTF-8?q?=E6=8D=AE=E6=9F=A5=E7=9C=8B=E5=8A=9F=E8=83=BD=E5=B9=B6=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E6=BC=94=E7=A4=BA=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 client.py 中新增 view() 函数,支持调用金蝶云星空 DynamicFormService.View 接口 - 在 tools.py 中新增 view_form 工具,供 MCP 客户端调用 - 添加演示脚本 demo_view.py 和示例配置文件 demo_view.json - 重构 demo_save.py 为通用演示脚本,支持自定义 payload 文件 - 删除过时的 demo.json,新增 userful_save.json 作为实用示例 - 添加项目交接文档,详细说明各子项目架构和配置 - 优化 payload 序列化逻辑,提取为共享函数 _serialize_payload --- k3cloud_mcp/k3cloud_mcp/client.py | 56 +- k3cloud_mcp/k3cloud_mcp/demo.json | 78 --- k3cloud_mcp/k3cloud_mcp/demo_save.json | 34 + .../k3cloud_mcp/{demo.py => demo_save.py} | 2 +- k3cloud_mcp/k3cloud_mcp/demo_view.json | 6 + k3cloud_mcp/k3cloud_mcp/demo_view.py | 72 ++ k3cloud_mcp/k3cloud_mcp/tools.py | 52 +- k3cloud_mcp/k3cloud_mcp/userful_save.json | 47 ++ k3cloud_mcp/k3cloud_mcp/view_result.json | 631 ++++++++++++++++++ 交接文档.md | 408 +++++++++++ 10 files changed, 1284 insertions(+), 102 deletions(-) delete mode 100644 k3cloud_mcp/k3cloud_mcp/demo.json create mode 100644 k3cloud_mcp/k3cloud_mcp/demo_save.json rename k3cloud_mcp/k3cloud_mcp/{demo.py => demo_save.py} (97%) create mode 100644 k3cloud_mcp/k3cloud_mcp/demo_view.json create mode 100644 k3cloud_mcp/k3cloud_mcp/demo_view.py create mode 100644 k3cloud_mcp/k3cloud_mcp/userful_save.json create mode 100644 k3cloud_mcp/k3cloud_mcp/view_result.json create mode 100644 交接文档.md diff --git a/k3cloud_mcp/k3cloud_mcp/client.py b/k3cloud_mcp/k3cloud_mcp/client.py index 6688aa0..0a77af0 100644 --- a/k3cloud_mcp/k3cloud_mcp/client.py +++ b/k3cloud_mcp/k3cloud_mcp/client.py @@ -12,6 +12,22 @@ def build_save_url(path: str) -> str: return f"{Config.BASE_URL.rstrip('/')}{path}" +def _serialize_payload(payload: Any, field_name: str) -> str: + if payload is None: + raise ValueError(f"{field_name} 不能为空") + if isinstance(payload, str): + payload_value = payload.strip() + if not payload_value: + raise ValueError(f"{field_name} 不能为空字符串") + return payload_value + try: + return json.dumps(payload, ensure_ascii=False) + except TypeError as exc: + raise TypeError( + f"{field_name} 必须是可序列化的 JSON 对象或 JSON 字符串" + ) from exc + + def save(formid: str, data_payload: Any, timeout: int = 30) -> Dict[str, Any]: """ Call the K3Cloud Save API. @@ -22,18 +38,7 @@ def save(formid: str, data_payload: Any, timeout: int = 30) -> Dict[str, Any]: normalized_formid = str(formid or "").strip() if not normalized_formid: raise ValueError("formid 不能为空") - if data_payload is None: - raise ValueError("data_payload 不能为空") - - if isinstance(data_payload, str): - payload_value = data_payload.strip() - if not payload_value: - raise ValueError("data_payload 不能为空字符串") - else: - try: - payload_value = json.dumps(data_payload, ensure_ascii=False) - except TypeError as exc: - raise TypeError("data_payload 必须是可序列化的 JSON 对象或 JSON 字符串") from exc + payload_value = _serialize_payload(data_payload, "data_payload") payload = { "formid": normalized_formid, @@ -48,3 +53,30 @@ def save(formid: str, data_payload: Any, timeout: int = 30) -> Dict[str, Any]: ) response.raise_for_status() return response.json() + + +def view(formid: str, payload: Any, timeout: int = 30) -> Dict[str, Any]: + """ + Call the K3Cloud View API. + + `payload` can be a JSON object or a pre-serialized JSON string. + """ + view_service_path = "/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.View.common.kdsvc" + normalized_formid = str(formid or "").strip() + if not normalized_formid: + raise ValueError("formid 不能为空") + payload_value = _serialize_payload(payload, "payload") + + request_body = { + "formid": normalized_formid, + "data": payload_value, + } + + response = requests.post( + build_save_url(view_service_path), + json=request_body, + headers=build_headers(view_service_path), + timeout=timeout, + ) + response.raise_for_status() + return response.json() diff --git a/k3cloud_mcp/k3cloud_mcp/demo.json b/k3cloud_mcp/k3cloud_mcp/demo.json deleted file mode 100644 index 68801f2..0000000 --- a/k3cloud_mcp/k3cloud_mcp/demo.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "NeedUpDateFields": [], - "NeedReturnFields": [], - "IsDeleteEntry": "true", - "SubSystemId": "", - "IsVerifyBaseDataField": "false", - "IsEntryBatchFill": "true", - "ValidateFlag": "true", - "NumberSearch": "true", - "IsAutoAdjustField": "false", - "InterationFlags": "", - "IgnoreInterationFlag": "", - "IsControlPrecision": "false", - "ValidateRepeatJson": "false", - "Model": { - "FID": 0, - "FBillTypeID": { - "FNUMBER": "" - }, - "FBillNo": "1", - "FPAYORGID": { - "FNumber": "" - }, - "FDATE": "1900-01-01", - "FCONTACTUNITTYPE": "", - "FCONTACTUNIT": { - "FNumber": "" - }, - "FPAYUNITTYPE": "", - "FPAYUNIT": { - "FNumber": "" - }, - "FCURRENCYID": { - "FNumber": "" - }, - "FSETTLECUR": { - "FNUMBER": "" - }, - "FDOCUMENTSTATUS": "", - "FBUSINESSTYPE": "", - "FCancelStatus": "", - "FSETTLEMAINBOOKID": { - "FNUMBER": "" - }, - "FRECEIVEBILLENTRY": [ - { - "FEntryID": 0, - "FSETTLETYPEID": { - "FNumber": "" - }, - "FPURPOSEID": { - "FNumber": "SFKYT01_SYS" - }, - "FPOSTDATE": "1900-01-01", - "FASSSALESORDER": [ - { - "FDetailID": 0 - } - ] - } - ], - "FRECEIVEBILLSRCENTRY": [ - { - "FEntryID": 0 - } - ], - "FBILLRECEIVABLEENTRY": [ - { - "FEntryID": 0 - } - ], - "FBILLSKDRECENTRY": [ - { - "FEntryID": 0 - } - ] - } -} \ No newline at end of file diff --git a/k3cloud_mcp/k3cloud_mcp/demo_save.json b/k3cloud_mcp/k3cloud_mcp/demo_save.json new file mode 100644 index 0000000..2e08910 --- /dev/null +++ b/k3cloud_mcp/k3cloud_mcp/demo_save.json @@ -0,0 +1,34 @@ +{ + "Model": { + "FID": 0, + "FBillTypeID": { + "FNUMBER": "SKDLX02_SYS" + }, + "FBillNo": "1211", + "FPAYORGID": { + "FNumber": "100" + }, + "FDATE": "2026-05-02", + "FCONTACTUNITTYPE": "BD_Empinfo", + "FCONTACTUNIT": { + "FNumber": "220804" + }, + "FPAYUNITTYPE": "BD_Empinfo", + "FCURRENCYID": { + "FNumber": "PRE001" + }, + "FRECEIVEBILLENTRY": [ + { + "FEntryID": 0, + "FRECAMOUNTFOR_E": 123.0, + "FRECTOTALAMOUNTFOR": 123456.0, + "FSETTLETYPEID": { + "FNumber": "JSFS01_SYS" + }, + "FPURPOSEID": { + "FNumber": "SFKYT004" + } + } + ] + } +} diff --git a/k3cloud_mcp/k3cloud_mcp/demo.py b/k3cloud_mcp/k3cloud_mcp/demo_save.py similarity index 97% rename from k3cloud_mcp/k3cloud_mcp/demo.py rename to k3cloud_mcp/k3cloud_mcp/demo_save.py index 1b6fbfa..1431bd0 100644 --- a/k3cloud_mcp/k3cloud_mcp/demo.py +++ b/k3cloud_mcp/k3cloud_mcp/demo_save.py @@ -8,7 +8,7 @@ from typing import Any CURRENT_DIR = Path(__file__).resolve().parent PROJECT_ROOT = CURRENT_DIR.parent -DEFAULT_PAYLOAD_PATH = CURRENT_DIR / "demo.json" +DEFAULT_PAYLOAD_PATH = CURRENT_DIR / "demo_save.json" MCP_SERVER_PATH = PROJECT_ROOT / "mcp-server.json" diff --git a/k3cloud_mcp/k3cloud_mcp/demo_view.json b/k3cloud_mcp/k3cloud_mcp/demo_view.json new file mode 100644 index 0000000..94b99ce --- /dev/null +++ b/k3cloud_mcp/k3cloud_mcp/demo_view.json @@ -0,0 +1,6 @@ +{ + "CreateOrgId": 0, + "Number": "SKD00126864", + "Id": "", + "IsSortBySeq": "false" +} \ No newline at end of file diff --git a/k3cloud_mcp/k3cloud_mcp/demo_view.py b/k3cloud_mcp/k3cloud_mcp/demo_view.py new file mode 100644 index 0000000..edca0c4 --- /dev/null +++ b/k3cloud_mcp/k3cloud_mcp/demo_view.py @@ -0,0 +1,72 @@ +import argparse +import json +import os +import sys +from pathlib import Path +from typing import Any + + +CURRENT_DIR = Path(__file__).resolve().parent +PROJECT_ROOT = CURRENT_DIR.parent +DEFAULT_PAYLOAD_PATH = CURRENT_DIR / "demo_view.json" +MCP_SERVER_PATH = PROJECT_ROOT / "mcp-server.json" + + +def _ensure_project_root_on_path() -> None: + root = str(PROJECT_ROOT) + if root not in sys.path: + sys.path.insert(0, root) + + +def _load_payload(payload_path: Path) -> Any: + raw_text = payload_path.read_text(encoding="utf-8").strip() + if not raw_text: + raise ValueError(f"payload 文件为空: {payload_path}") + return json.loads(raw_text) + + +def _bootstrap_env_from_mcp_server() -> None: + if not MCP_SERVER_PATH.exists(): + return + + data = json.loads(MCP_SERVER_PATH.read_text(encoding="utf-8")) + env_map = data.get("mcpServers", {}).get("k3cloud-mcp", {}).get("env", {}) + for key, value in env_map.items(): + if key not in os.environ and value is not None: + os.environ[key] = str(value) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="读取 payload JSON 并直接调用 k3cloud_mcp.client.view()" + ) + parser.add_argument("formid", help="K3Cloud 表单标识,例如 BD_Customer") + parser.add_argument( + "--payload", + default=str(DEFAULT_PAYLOAD_PATH), + help="payload JSON 文件路径,默认读取同级 demo_view.json", + ) + parser.add_argument( + "--timeout", + type=int, + default=30, + help="请求超时时间,单位秒,默认 30", + ) + args = parser.parse_args() + + payload_path = Path(args.payload).expanduser().resolve() + _ensure_project_root_on_path() + _bootstrap_env_from_mcp_server() + + from k3cloud_mcp.client import view + + result = view( + formid=args.formid, + payload=_load_payload(payload_path), + timeout=args.timeout, + ) + print(json.dumps(result, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/k3cloud_mcp/k3cloud_mcp/tools.py b/k3cloud_mcp/k3cloud_mcp/tools.py index 16bd940..83a0559 100644 --- a/k3cloud_mcp/k3cloud_mcp/tools.py +++ b/k3cloud_mcp/k3cloud_mcp/tools.py @@ -3,7 +3,7 @@ from typing import Any, Dict, List from mcp.types import TextContent, Tool -from .client import save +from .client import save, view tools = [ Tool( @@ -28,17 +28,40 @@ tools = [ }, "required": ["formid", "data_payload"], }, - ) + ), + Tool( + name="view_form", + description="调用金蝶云星空 DynamicFormService.View 接口查看单据。", + inputSchema={ + "type": "object", + "properties": { + "formid": { + "type": "string", + "description": "业务对象表单 Id,例如 SAL_QUOTATION", + }, + "payload": { + "description": "View 接口的 payload 参数,支持 JSON 对象或 JSON 字符串", + }, + "timeout": { + "type": "integer", + "description": "HTTP 超时时间(秒),默认 30", + "default": 30, + "minimum": 1, + }, + }, + "required": ["formid", "payload"], + }, + ), ] -def _normalize_payload(value: object) -> Any: +def _normalize_payload(value: object, field_name: str) -> Any: if value is None: - raise ValueError("missing data_payload") + raise ValueError(f"missing {field_name}") if isinstance(value, str): normalized = value.strip() if not normalized: - raise ValueError("missing data_payload") + raise ValueError(f"missing {field_name}") try: return json.loads(normalized) except json.JSONDecodeError: @@ -48,7 +71,7 @@ def _normalize_payload(value: object) -> Any: async def handle_call_tool(name: str, arguments: Dict[str, object]) -> List[TextContent]: try: - if name != "save_form": + if name not in {"save_form", "view_form"}: raise ValueError(f"unknown tool name: {name}") formid = str(arguments.get("formid", "")).strip() @@ -60,11 +83,18 @@ async def handle_call_tool(name: str, arguments: Dict[str, object]) -> List[Text if timeout <= 0: raise ValueError("timeout must be greater than 0") - result = save( - formid=formid, - data_payload=_normalize_payload(arguments.get("data_payload")), - timeout=timeout, - ) + if name == "save_form": + result = save( + formid=formid, + data_payload=_normalize_payload(arguments.get("data_payload"), "data_payload"), + timeout=timeout, + ) + else: + result = view( + formid=formid, + payload=_normalize_payload(arguments.get("payload"), "payload"), + timeout=timeout, + ) return [ TextContent( type="text", diff --git a/k3cloud_mcp/k3cloud_mcp/userful_save.json b/k3cloud_mcp/k3cloud_mcp/userful_save.json new file mode 100644 index 0000000..caf2ca9 --- /dev/null +++ b/k3cloud_mcp/k3cloud_mcp/userful_save.json @@ -0,0 +1,47 @@ +{ + "NeedUpDateFields": [], + "NeedReturnFields": [], + "IsDeleteEntry": "true", + "SubSystemId": "", + "IsVerifyBaseDataField": "false", + "IsEntryBatchFill": "true", + "ValidateFlag": "true", + "NumberSearch": "true", + "IsAutoAdjustField": "false", + "InterationFlags": "", + "IgnoreInterationFlag": "", + "IsControlPrecision": "false", + "ValidateRepeatJson": "false", + "Model": { + "FID": 0, + "FBillTypeID": { + "FNUMBER": "SKDLX02_SYS" "--其他业务收款单" + }, + "FBillNo": "3", "--单据编号" + "FPAYORGID": { + "FNumber": "100" "--收款组织" + }, + "FDATE": "2026-05-02", "--业务日期" + "FCONTACTUNITTYPE": "BD_Empinfo", "--往来单位类型" + "FCONTACTUNIT": { + "FNumber": "220804" "--往来单位" + }, + "FPAYUNITTYPE": "BD_Empinfo", "--付款单位类型" + "FCURRENCYID": { + "FNumber": "PRE001" "--币别" + }, + "FRECEIVEBILLENTRY": [ + { + "FEntryID": 0, + "FRECAMOUNTFOR_E": 123.0, "--收款金额" + "FRECTOTALAMOUNTFOR": 123456.0, "--应收金额" + "FSETTLETYPEID": { + "FNumber": "JSFS01_SYS" "--结算方式 JSFS01_SYS 现金、JSFS04_SYS 电汇" + }, + "FPURPOSEID": { + "FNumber": "SFKYT004" "--收款用途 SFKYT10 内部往来-费用往来、SFKYT004 借支还款" + } + } + ] + } +} diff --git a/k3cloud_mcp/k3cloud_mcp/view_result.json b/k3cloud_mcp/k3cloud_mcp/view_result.json new file mode 100644 index 0000000..06e0e38 --- /dev/null +++ b/k3cloud_mcp/k3cloud_mcp/view_result.json @@ -0,0 +1,631 @@ +{ + "Result": { + "ResponseStatus": { + "IsSuccess": true + }, + "Result": { + "Id": 263989, + "BILLNo": "SKD00126001", + "DOCUMENTSTATUS": "C", + "FCreatorId_Id": 121428040, + "FCreatorId": { + "Id": 121428040, + "Name": "杨苏梅", + "UserAccount": "杨苏梅" + }, + "APPROVERID_Id": 676841452, + "APPROVERID": { + "Id": 676841452, + "Name": "郑旭君", + "UserAccount": "郑旭君" + }, + "FCreateDate": "2025-01-14T14:56:23.627", + "SETTLEORGID_Id": 1, + "SETTLEORGID": { + "Id": 1, + "MultiLanguageText": [ + { + "PkId": 1, + "LocaleId": 2052, + "Name": "海圆圆集团" + }, + { + "PkId": 100001, + "LocaleId": 1033, + "Name": "海圆圆集团" + }, + { + "PkId": 100002, + "LocaleId": 3076, + "Name": "海圆圆集团" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "海圆圆集团" + }, + { + "Key": 1033, + "Value": "海圆圆集团" + }, + { + "Key": 3076, + "Value": "海圆圆集团" + } + ], + "Number": "100", + "FRadio": null, + "ParentOrg_Id": 1, + "ParentOrg": { + "Id": 1, + "Number": "100", + "MultiLanguageText": [ + { + "PkId": 1, + "LocaleId": 2052, + "Name": "海圆圆集团" + }, + { + "PkId": 100001, + "LocaleId": 1033, + "Name": "海圆圆集团" + }, + { + "PkId": 100002, + "LocaleId": 3076, + "Name": "海圆圆集团" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "海圆圆集团" + }, + { + "Key": 1033, + "Value": "海圆圆集团" + }, + { + "Key": 3076, + "Value": "海圆圆集团" + } + ] + } + }, + "ApproveDate": "2025-01-15T08:53:57.68", + "SALEORGID_Id": 0, + "SALEORGID": null, + "EXCHANGETYPE_Id": 1, + "EXCHANGETYPE": { + "Id": 1, + "msterID": 1, + "MultiLanguageText": [ + { + "PkId": 1, + "LocaleId": 2052, + "Name": "固定汇率" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "固定汇率" + } + ], + "Number": "HLTX01_SYS", + "Digits": 4, + "ReverseDigits": 4 + }, + "RECEIVEAMOUNTFOR": 820000.0, + "MAINBOOKCURID_Id": 1, + "MAINBOOKCURID": { + "Id": 1, + "msterID": 1, + "MultiLanguageText": [ + { + "PkId": 2, + "LocaleId": 2052, + "Name": "人民币" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "人民币" + } + ], + "Number": "PRE001", + "Sysmbol": "¥", + "PriceDigits": 6, + "AmountDigits": 2, + "IsShowCSymbol": true, + "FormatOrder": "1", + "RoundType": "1" + }, + "RECEIVEAMOUNT": 820000.0, + "CURRENCYID_Id": 1, + "CURRENCYID": { + "Id": 1, + "msterID": 1, + "MultiLanguageText": [ + { + "PkId": 2, + "LocaleId": 2052, + "Name": "人民币" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "人民币" + } + ], + "Number": "PRE001", + "Sysmbol": "¥", + "PriceDigits": 6, + "AmountDigits": 2, + "IsShowCSymbol": true, + "FormatOrder": "1", + "RoundType": "1" + }, + "DATE": "2025-01-02T00:00:00", + "FModifyDate": "2025-01-15T08:53:55.51", + "ModifierId_Id": 676841452, + "ModifierId": { + "Id": 676841452, + "Name": "郑旭君", + "UserAccount": "郑旭君" + }, + "EXCHANGERATE": 1.0, + "WRITTENOFFSTATUS": "A", + "SALEERID_Id": 0, + "SALEERID": null, + "SALEGROUPID_Id": 0, + "SALEGROUPID": null, + "SALEDEPTID_Id": 0, + "SALEDEPTID": null, + "BillTypeID_Id": "670b3608188c425a9f696d7ccb47843d", + "BillTypeID": { + "Id": "670b3608188c425a9f696d7ccb47843d", + "MultiLanguageText": [ + { + "PkId": "557479e6741c42469036c5548c9d9407", + "LocaleId": 2052, + "Name": "其他业务收款单" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "其他业务收款单" + } + ], + "Number": "SKDLX02_SYS" + }, + "REALRECAMOUNTFOR": 820000.0, + "REALRECAMOUNT": 820000.0, + "ACCOUNTSYSTEM_Id": 0, + "ACCOUNTSYSTEM": null, + "CancelDate": null, + "CancelStatus": "A", + "CancellerId_Id": 0, + "CancellerId": null, + "CONTACTUNITTYPE": "BD_Empinfo", + "CONTACTUNIT_Id": 279577322, + "CONTACTUNIT": { + "Id": 279577322, + "msterID": 279577322, + "MultiLanguageText": [ + { + "PkId": 102684, + "LocaleId": 2052, + "Name": "吴喜燕" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "吴喜燕" + } + ], + "Number": "220804" + }, + "PAYUNITTYPE": "BD_Empinfo", + "PAYUNIT_Id": 279577322, + "PAYUNIT": { + "Id": 279577322, + "msterID": 279577322, + "MultiLanguageText": [ + { + "PkId": 102684, + "LocaleId": 2052, + "Name": "吴喜燕" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "吴喜燕" + } + ], + "Number": "220804" + }, + "BUSINESSTYPE": "3", + "CreditCheckResult": "0", + "ISINIT": false, + "Department_Id": 0, + "Department": null, + "FPAYORGID_Id": 1, + "FPAYORGID": { + "Id": 1, + "MultiLanguageText": [ + { + "PkId": 1, + "LocaleId": 2052, + "Name": "海圆圆集团" + }, + { + "PkId": 100001, + "LocaleId": 1033, + "Name": "海圆圆集团" + }, + { + "PkId": 100002, + "LocaleId": 3076, + "Name": "海圆圆集团" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "海圆圆集团" + }, + { + "Key": 1033, + "Value": "海圆圆集团" + }, + { + "Key": 3076, + "Value": "海圆圆集团" + } + ], + "Number": "100", + "FRadio": null, + "ParentOrg_Id": 1, + "ParentOrg": { + "Id": 1, + "Number": "100", + "MultiLanguageText": [ + { + "PkId": 1, + "LocaleId": 2052, + "Name": "海圆圆集团" + }, + { + "PkId": 100001, + "LocaleId": 1033, + "Name": "海圆圆集团" + }, + { + "PkId": 100002, + "LocaleId": 3076, + "Name": "海圆圆集团" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "海圆圆集团" + }, + { + "Key": 1033, + "Value": "海圆圆集团" + }, + { + "Key": 3076, + "Value": "海圆圆集团" + } + ] + } + }, + "FIsSameOrg": true, + "FSOURCESYSTEM": "0", + "FCASHSALE": false, + "SETTLECUR_Id": 1, + "SETTLECUR": { + "Id": 1, + "msterID": 1, + "MultiLanguageText": [ + { + "PkId": 2, + "LocaleId": 2052, + "Name": "人民币" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "人民币" + } + ], + "Number": "PRE001", + "Sysmbol": "¥", + "PriceDigits": 6, + "AmountDigits": 2, + "IsShowCSymbol": true, + "FormatOrder": "1", + "RoundType": "1" + }, + "SETTLERATE": 1.0, + "RECAMOUNTFOR": 820000.0, + "RECAMOUNT": 820000.0, + "ISB2C": false, + "WBSETTLENO": " ", + "IsWriteOff": false, + "MatchMethodID": 0, + "FScanPoint_Id": 0, + "FScanPoint": null, + "FKDPAYNO": " ", + "FREMARK": " ", + "FTHIRDBILLNO": " ", + "FSETTLEMAINBOOKID_Id": 1, + "FSETTLEMAINBOOKID": { + "Id": 1, + "msterID": 1, + "MultiLanguageText": [ + { + "PkId": 2, + "LocaleId": 2052, + "Name": "人民币" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "人民币" + } + ], + "Number": "PRE001", + "Sysmbol": "¥", + "PriceDigits": 6, + "AmountDigits": 2, + "IsShowCSymbol": true, + "FormatOrder": "1", + "RoundType": "1" + }, + "FSETTLEEXCHANGETYPE_Id": 1, + "FSETTLEEXCHANGETYPE": { + "Id": 1, + "msterID": 1, + "MultiLanguageText": [ + { + "PkId": 1, + "LocaleId": 2052, + "Name": "固定汇率" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "固定汇率" + } + ], + "Number": "HLTX01_SYS", + "Digits": 4, + "ReverseDigits": 4 + }, + "FOUTCONTACTID_Id": 0, + "FOUTCONTACTID": null, + "FOUTCONTACTTYPE": " ", + "FGYACCOUNTWATERID": 0, + "FISCARRYRATE": false, + "PRESETBASE1_Id": 0, + "PRESETBASE1": null, + "PRESETBASE2_Id": 0, + "PRESETBASE2": null, + "FPRESETASSISTANT1_Id": " ", + "FPRESETASSISTANT1": null, + "FPRESETASSISTANT2_Id": " ", + "FPRESETASSISTANT2": null, + "FPRESETTEXT1": " ", + "FPRESETTEXT2": " ", + "FISFROMSALORDER": false, + "FVirIsSameAcctOrg": false, + "SourceBillNumber": " ", + "RECEIVEBILLENTRY": [ + { + "Id": 288025, + "Seq": 1, + "SETTLETYPEID_Id": 1, + "SETTLETYPEID": { + "Id": 1, + "msterID": 1, + "MultiLanguageText": [ + { + "PkId": 1, + "LocaleId": 2052, + "Name": "现金" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "现金" + } + ], + "Number": "JSFS01_SYS", + "SETTLECATEGORY": "1", + "TYPE": "1", + "FRECBILLTYPE": " ", + "FACCOUNTTYPE": " " + }, + "SETTLERECAMOUNTFOR": 820000.0, + "SETTLEDISTAMOUNTFOR": 0.0, + "RECTOTALAMOUNTFOR": 820000.0, + "SETTLERECAMOUNT": 820000.0, + "SETTLEDISTAMOUNT": 0.0, + "RECTOTALAMOUNT": 820000.0, + "WRITTENOFFSTATUS": "A", + "WRITTENOFFAMOUNTFOR": 0.0, + "COMMENT": "收到吴喜燕归还借支款", + "OPPOSITEBANKACCOUNT": " ", + "OPPOSITECCOUNTNAME": " ", + "RECEIVEITEM": " ", + "HANDLINGCHARGEFOR": 0.0, + "HANDLINGCHARGE": 0.0, + "REALRECAMOUNTFOR": 820000.0, + "FREALRECAMOUNT": 820000.0, + "ASSTOTALAMOUNTFOR": 0.0, + "RECEIVEITEMTYPE": " ", + "SaleOrderID": 0, + "ACCOUNTID_Id": 0, + "ACCOUNTID": null, + "OVERUNDERAMOUNTFOR": 0.0, + "OPPOSITEBANKNAME": " ", + "SETTLENO": " ", + "BLEND": false, + "PURPOSEID_Id": 266819, + "PURPOSEID": { + "Id": 266819, + "MultiLanguageText": [ + { + "PkId": 100005, + "LocaleId": 2052, + "Name": "借支还款" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "借支还款" + } + ], + "Number": "SFKYT004", + "RECPAYTYPE": "0", + "BUSINESSTYPE": "3", + "FINMANEGEMENT": true, + "FPAYMENTADVANCE": false + }, + "OVERUNDERAMOUNT": 0.0, + "FINNERACCOUNTID_Id": 0, + "FINNERACCOUNTID": null, + "ReFundAmount": 0.0, + "CashAccount_Id": 100402, + "CashAccount": { + "Id": 100402, + "msterID": 100402, + "MultiLanguageText": [ + { + "PkId": 100001, + "LocaleId": 2052, + "Name": "海圆圆" + } + ], + "Name": [ + { + "Key": 2052, + "Value": "海圆圆" + } + ], + "Number": "海圆圆" + }, + "RECAMOUNTFOR_E": 820000.0, + "RECAMOUNT_E": 820000.0, + "FPOSTDATE": "2025-01-02T00:00:00", + "FISPOST": true, + "FMATERIALID_Id": 0, + "FMATERIALID": null, + "FSALEORDERNO": " ", + "FMATERIALSEQ": 0, + "FORDERENTRYID": 0, + "TOPAYMENTAMOUNTFOR": 0.0, + "FWRITTENOFFAMOUNT": 0.0, + "FNOTVERIFICATEAMOUNT": 820000.0, + "FPRICEUNITID_Id": 0, + "FPRICEUNITID": null, + "FPrice": 0.0, + "FQty": 0.0, + "COSTID_Id": 0, + "COSTID": null, + "COSTDEPARTMENTID_Id": 0, + "COSTDEPARTMENTID": null, + "FLINKROWID": " ", + "FSALEORDERBASE_Id": 0, + "FSALEORDERBASE": null, + "FRelateReFundAmount": 0.0, + "ENTRYTAXRATE": 0.0, + "TAXAMOUNTFOR": 0.0, + "TAXAMOUNT": 0.0, + "RECNOTAXAMOUNTFOR": 820000.0, + "RECNOTAXAMOUNT": 820000.0, + "WRITTENOFFTAXAMOUNTFOR": 0.0, + "FWRITTENOFFTAXAMOUNT": 0.0, + "FSALEORDERNUMBER": null, + "AR_ASSSALESORDER": [] + } + ], + "RECEIVEBILLSRCENTRY": [ + { + "Id": 0, + "Seq": 1, + "SRCBILLID": 0, + "AFTTAXTOTALAMOUNT": 0.0, + "SRCSETTLETYPEID_Id": 0, + "SRCSETTLETYPEID": null, + "SRCCURRENCYID_Id": 0, + "SRCCURRENCYID": null, + "EXPIRY": null, + "PLANRECAMOUNT": 0.0, + "REALRECAMOUNT": 0.0, + "SRCBILLTYPEID": "", + "SRCBILLNO": null, + "SRCSEQ": 0, + "SRCROWID": 0, + "ORDERBILLNO": null, + "FSRCMATERIALID_Id": 0, + "FSRCMATERIALID": null, + "FSRCMATERIALSEQ": 0, + "FSRCORDERENTRYID": 0, + "FSETTLEAMOUNT": 0.0, + "FREALRECAMOUNTFOR": 0.0, + "FSRCPRICEUNITID_Id": 0, + "FSRCPRICEUNITID": null, + "FSRCPRICE": 0.0, + "FSRCQTY": 0.0, + "SRCCOSTID_Id": 0, + "SRCCOSTID": null, + "SRCCOSTDEPARTMENTID_Id": 0, + "SRCCOSTDEPARTMENTID": null, + "FSRCLINKROWID": null, + "SRCREMARK": null, + "FASSORDERAMOUNT": 0.0, + "FSRCPURPOSEID_Id": 0, + "FSRCPURPOSEID": null, + "FRECEIVEBILLSRCENTRY_Link": [] + } + ], + "RECEIVEBILLREC": [ + { + "Id": 0, + "Seq": 1, + "BILLID_Id": 0, + "BILLID": null, + "PARLEFTAMOUNTFOR": 0.0, + "USEDAMOUNTFOR": 0.0, + "FInnerAccountID_B_Id": 0, + "FInnerAccountID_B": null, + "FTempOrgId_Id": 0, + "FTempOrgId": null, + "FBPBILLNUMBER": null, + "BILLPARAMOUNT": 0.0, + "PARLEFTAMOUNTSTD": 0.0, + "USEDAMOUNTSTD": 0.0, + "FBPBILLPARAMOUNT": 0.0 + } + ], + "BILLSKDRECEIVABLEENTRY": [], + "BOS_ConvertTakeDataInfo": null + } + } +} \ No newline at end of file diff --git a/交接文档.md b/交接文档.md new file mode 100644 index 0000000..9147294 --- /dev/null +++ b/交接文档.md @@ -0,0 +1,408 @@ +# lzwcai-mcp 项目交接文档 + +本文档面向接手本仓库的技术人员,重点说明整体架构、各子项目职责、运行方式与配置要点,便于快速上手维护与扩展。 + +## 一、项目整体概览 + +- 仓库名称:`lzwcai-mcp` +- 项目类型:一组基于 MCP(Model Context Protocol)的独立服务,每个服务都是一个「技能包」,通过 `stdio` 与上层 MCP Agent 通信。 +- 主要能力: + - Feishu/Lark 卡片发送、图片上传、OKR 查询等企业协同能力(`lzwcai_lark_mcp`) + - 文件与 URL 的 Base64 编解码、临时文件处理、MinIO 上传等通用文件工具(`file_tools`) + - 金蝶云星空单据保存与查询能力,封装 DynamicFormService.Save / View 接口(`k3cloud_mcp`) + - 访客信息查询与登记、门禁控制(`terminal_dhr_mcp`) + - Temi 轮足机器人导航与业务动作控制(`terminal_temi_mcp`) + - Go2 机器人运动控制、避障、服务管理等(`terminal_go2_mcp`) + +各子项目都是独立的 Python 包,拥有各自的 `pyproject.toml` 与入口脚本,通过 JSON 配置文件被 MCP Agent 挂载为工具服务器。 + +## 二、代码结构概览 + +仓库根目录主要结构: + +- `file_tools/` + - `file_tools/`:文件工具 MCP 服务器实现(入口 `file_tools.main`)。 + - `pyproject.toml`:使用 `hatchling` 构建,脚本名 `lzwcai-file-tools-mcp`。 +- `k3cloud_mcp/` + - `k3cloud_mcp/`:金蝶云星空 MCP 服务器实现。 + - `mcp-server.json`:MCP Agent 挂载示例,包含 `k3cloud-mcp` 的启动方式与环境变量示例。 + - `pyproject.toml`:使用 `hatchling` 构建,脚本名 `k3cloud-mcp`。 +- `lzwcai_lark_mcp/` + - `lzwcai_lark_mcp/`:Lark 相关 MCP 服务器实现(入口 `lzwcai_lark_mcp.main`)。 + - `pyproject.toml`:使用 `hatchling` 构建,脚本名 `lzwcai-lark-mcp`。 +- `lzwcai_temi_mcp/` + - `lzwcai_temi_mcp/`:另一个 Temi 机器人相关 MCP 服务(目前主要用到 `main.py`、`mcp_mqtt.py`、`nav_server.py`)。 + - `mcp-server.json`:MCP Agent 挂载示例。 + - `pyproject.toml`:使用 `hatchling` 构建,脚本名 `lzwcai_temi_mcp`。 +- `terminal_dhr_mcp/` + - `terminal_dhr_mcp/`:访客系统 + 门禁控制 MCP 服务器。 + - `.env`:示例环境变量。 + - `pyproject.toml`:基于 `setuptools` 的构建配置,脚本名 `terminal-dhr-mcp`。 +- `terminal_go2_mcp/` + - `terminal_go2_mcp/`:Go2 机器人 MCP 服务器。 + - `pyproject.toml`:使用 `hatchling` 构建,脚本名 `terminal_go2_mcp`。 +- `terminal_temi_mcp/` + - `terminal_temi_mcp/`:Temi 轮足机器人 MCP 服务器。 + - `pyproject.toml`:使用 `hatchling` 构建,脚本名 `terminal_temi_mcp`。 +- 根目录其他文件: + - `README.md`:顶层简介与部分子项目说明。 + - `mcp.json`:示例 MCP 服务器配置(目前包含一个 JS 实现的 `lark-mcp` 示例,与本仓库 Python 版本的 Lark MCP 可共存)。 + - `LICENSE`、`.gitignore` 等。 + +## 三、运行环境与基础依赖 + +总体建议: + +- Python 版本:优先使用 **3.10+**。 + - `terminal-dhr-mcp` 声明支持 3.8+,但其他项目为 3.10+,统一到 3.10 可以减少环境碎片。 +- 依赖管理:建议使用虚拟环境(`venv` 或 `uv` 等)分别在各子项目目录下执行安装。 + +核心依赖: + +- 统一: + - `mcp[cli]` 或 `mcp`:MCP 协议实现。 + - `python-dotenv`:从 `.env` 加载配置。 +- Lark 相关(`lzwcai_lark_mcp`): + - `requests`:调用 Feishu/Lark HTTP API。 + - `openpyxl`:从 Excel 中解析 `image_key` 及图片路径。 +- 文件工具(`file_tools`): + - `httpx`:下载远程文件。 + - `minio`:将文件上传到 MinIO 对象存储。 + - `openpyxl`:Excel 图片解析。 +- 金蝶云星空(`k3cloud_mcp`): + - `requests`:调用 K3Cloud Web API。 +- 访客/门禁(`terminal_dhr_mcp`): + - `httpx`、`pydantic`、`python-dotenv` 等,用于 HTTP 调用与配置。 +- 机器人相关(`terminal_temi_mcp`、`terminal_go2_mcp`): + - `fastapi`、`uvicorn`(部分场景)、`paho-mqtt`、`pydantic`、`requests`。 + +## 四、各子项目详细说明 + +### 4.1 Lark MCP(`lzwcai_lark_mcp`) + +- 入口: + - 包:`lzwcai_lark_mcp` + - 入口函数:`lzwcai_lark_mcp.main:main` + - MCP Server 名称:`lzwcai-mcpskills-lark-mcp` +- 核心文件: + - `main.py`:MCP server 启动逻辑,负责: + - 初始化 `Server`。 + - 通过 `_request_token` 根据 `app_id` / `app_secret` 获取 Lark `tenant_access_token`,带过期时间缓存。 + - 注册 `list_tools` / `call_tool`,在调用工具前自动确保 token 有效。 + - `config.py`: + - 从环境变量读取: + - `app_id` + - `app_secret` + - `tools.py`: + - 定义对外暴露的 `Tool` 列表。 + - 实现具体业务函数,如: + - 图片上传:`upload_image_by_url` / `upload_image_by_file` + - Excel 定位图片源:`_resolve_image_source_from_excel` + - OKR 查询:`get_user_okr_list`、`batch_get_okr` + - 资产确认/复核/陌生人处理/反馈等多种交互卡片:`send_confirmation_card`、`send_review_card`、`send_stranger_card`、`send_feedback_card` + - 入库通知卡片:`send_notion_card` / `send_card_message` + - `handle_call_tool`:根据 `name` 分发到对应的 Python 函数,处理参数校验、错误返回等。 +- 外部依赖与接口: + - Feishu/Lark Open API: + - 认证:`/open-apis/auth/v3/tenant_access_token/internal` + - 消息/图片:`/open-apis/im/v1/images`、`/open-apis/im/v1/messages` + - 通讯录:`/open-apis/contact/v3/users/{user_id}` + - OKR:`/open-apis/okr/v1/users/{user_id}/okrs`、`/open-apis/okr/v1/okrs/batch_get` +- 配置要点: + - 必需环境变量: + - `app_id`:Lark 应用的 app id。 + - `app_secret`:Lark 应用的 app secret。 + - 可选:`LOG_LEVEL`(通过 logging.basicConfig 读取)。 + +### 4.2 文件工具 MCP(`file_tools`) + +- 入口: + - 包:`file_tools` + - 入口函数:`file_tools.main:main` + - MCP Server 名称:`lzwcai-mcpskills-file-tools-mcp` +- 核心文件: + - `main.py`: + - 初始化 MCP `Server`,注册 `list_tools` / `call_tool`。 + - `tools.py`: + - 工具列表: + - `file_to_json`:将文件对象或本地路径转成 JSON 结构(带 base64 内容)。 + - `file_to_data_uri`:将文件转为 `data URI`。 + - `file_path_to_data_uri`:本地文件路径 -> `data URI`。 + - `url_to_data_uri`:URL 下载并转为 `data URI`。 + - `url_to_temp_file`:URL 下载为本地临时文件,返回路径。 + - `excel_image_key_to_temp_file`:从 Excel 中按 `image_key` 找到图片并写入临时文件。 + - `upload_file_to_minio`:将文件上传到 MinIO,返回访问 URL。 + - 工具内部大量使用 `Path`、`tempfile`、`httpx`、`minio` 等。 +- 配置要点: + - MinIO 相关配置位于 `config.py`(`minio_endpoint`、`minio_access_key`、`minio_secret_key`),通过环境变量读取。 + - 当前 `upload_file_to_minio` 里的 bucket 名称固定为 `lzwcai`(写在 `tools.py`),如需多环境隔离建议改为可配置项。 + +### 4.3 金蝶云星空 MCP(`k3cloud_mcp`) + +- 入口: + - 包:`k3cloud_mcp` + - 入口函数:`k3cloud_mcp.main:main` + - MCP Server 名称:`k3cloud-mcp` +- 核心文件: + - `main.py`: + - 初始化 MCP `Server`。 + - 注册 `list_tools` / `call_tool`。 + - 运行 `stdio_server`,供上层 MCP Agent 调用。 + - `tools.py`: + - 当前暴露两个工具:`save_form`、`view_form`。 + - `save_form` 参数: + - `formid`:业务对象表单 Id,例如 `SAL_QUOTATION`。 + - `data_payload`:可传 JSON 对象或 JSON 字符串。 + - `timeout`:HTTP 超时时间,默认 30 秒。 + - `view_form` 参数: + - `formid`:业务对象表单 Id,例如 `SAL_QUOTATION`。 + - `payload`:可传 JSON 对象或 JSON 字符串。 + - `timeout`:HTTP 超时时间,默认 30 秒。 + - `handle_call_tool()` 负责做参数校验、分发到底层 `save()` / `view()`,并把响应序列化为文本返回。 + - `client.py`: + - 封装 `save(formid, data_payload, timeout=30)` 与 `view(formid, payload, timeout=30)`。 + - 分别调用 `DynamicFormService.Save.common.kdsvc` 与 `DynamicFormService.View.common.kdsvc` 接口。 + - 会自动将对象型 payload 转为 JSON 字符串后再提交。 + - `signing.py`: + - 负责生成 K3Cloud 所需签名头。 + - 包含 `APP_ID` 拆分、`APP_SECRET` 解码、HMAC 签名等逻辑。 + - `config.py`: + - 从环境变量读取 `K3CLOUD_*` 配置。 + - 缺少必填项时会在导入阶段直接抛出异常。 + - `demo_save.py` / `demo_save.json`: + - 用于本地直接测试 `save()` 函数。 + - `demo_view.py` / `demo_view.json`: + - 用于本地直接测试 `view()` 函数。 +- 外部依赖与接口: + - 金蝶云星空 Web API: + - 保存接口:`/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Save.common.kdsvc` + - 查询接口:`/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.View.common.kdsvc` +- 配置要点: + - 必需环境变量: + - `K3CLOUD_BASE_URL`:K3Cloud 站点根地址。 + - `K3CLOUD_ACCT_ID`:账套标识。 + - `K3CLOUD_APP_ID`:应用标识。 + - `K3CLOUD_USERNAME`:调用用户名。 + - `K3CLOUD_APP_SECRET`:应用密钥。 + - 可选环境变量: + - `K3CLOUD_LCID`:语言代码,默认 `2052`。 + - `K3CLOUD_ORG_NUM`:组织编号,默认 `0`。 + - `LOG_LEVEL`:日志级别。 +- 本地测试方式: + - 保存接口联调: + - `python k3cloud_mcp/demo_save.py --payload k3cloud_mcp/demo_save.json` + - 查询接口联调: + - `python k3cloud_mcp/demo_view.py --payload k3cloud_mcp/demo_view.json` + - 当前测试脚本会优先使用当前进程环境变量;若未设置,则补读子项目目录下 `mcp-server.json` 中 `k3cloud-mcp.env` 的配置,便于本地快速验证。 + +### 4.4 访客系统 + 门禁 MCP(`terminal_dhr_mcp`) + +- 入口: + - 包:`terminal_dhr_mcp` + - 入口函数:`terminal_dhr_mcp.main:main` + - MCP Server 名称:`terminal-dhr-mcp` +- 核心文件: + - `main.py`: + - 定义 `DhrMCPServer` 类,注册 `list_tools` / `call_tool`,调用 `tools.py` 中的逻辑。 + - `tools.py`: + - 工具列表包括: + - `query_visitor`:根据 `user_id` 查询访客预约记录。 + - `register_visitor`:登记新访客。 + - `control_door`:控制门禁开关。 + - `get_door_status`:查询门禁状态。 + - `config.py`: + - 关键配置: + - `EMPLOYEE_ID`:从环境变量 `employeeId` 读取,用于请求访客系统。 + - `BASE_URL`:访客系统服务地址。 + - `VISITOR_QUERY_ENDPOINT`、`VISITOR_REGISTER_ENDPOINT`:固定后缀路径。 + - 门禁相关: + - `DOOR_API_ENDPOINT` + - `DOOR_ENTERPRISE_ID` + - `DOOR_DEFAULT_ENTITY_ID` + - `LOG_LEVEL`、若干长度限制常量等。 +- 典型环境配置: + - 可在 `terminal_dhr_mcp/.env` 中配置: + - `employeeId` + - `BASE_URL` + - `DOOR_ENTERPRISE_ID` + - `DOOR_DEFAULT_ENTITY_ID` + - `LOG_LEVEL` + +### 4.5 Temi 轮足机器人 MCP(`terminal_temi_mcp`) + +- 入口: + - 包:`terminal_temi_mcp` + - 入口函数:`terminal_temi_mcp.main:main` + - MCP Server 名称:`terminal_temi_mcp` +- 核心文件: + - `main.py`: + - 定义 `NavServer`,通过 `mcp_mqtt.get_mcpmqtt_handler()` 获取 MQTT 客户端。 + - 封装业务命令: + - `nav_to`:导航到指定地点。 + - `speak`:语音播报。 + - `reception`:迎宾。 + - `notification`(目前工具层注释掉)。 + - `repose`:重新定位。 + - `delivery`:配送任务。 + - `patrol`:巡逻。 + - 若干人脸识别 / 扫码相关函数目前未对外暴露。 + - `list_tools` 中暴露的工具包括 `nav_to`、`speak`、`reception`、`repose`、`delivery`、`patrol` 等,对输入参数做了详细描述。 + - `call_tool` 根据工具名分发到 `NavServer` 方法,并做参数校验与错误转换。 + - `mcp_mqtt.py`: + - 封装 MQTT Broker 连接、客户端初始化与复用。 +- 配置要点: + - `.env`(参考 README): + - `LOG_LEVEL` + - `MQTT_BROKER_ADDRESS` + - `MQTT_PORT` + - `MQTT_USERNAME` + - `MQTT_PASSWORD` + - MCP Agent 配置文件(示例在子项目 README 中)会把 `employeeId`、`userId` 作为环境变量注入。 + +### 4.6 Go2 机器人 MCP(`terminal_go2_mcp`) + +- 入口: + - 包:`terminal_go2_mcp` + - 入口函数:`terminal_go2_mcp.main:main` + - MCP Server 名称:`terminal_go2_mcp` +- 核心文件: + - `main.py`: + - `TerminalGo2McpServer` 提供对 Go2 机器人的封装: + - 低层统一接口 `pubCmd(type, cmd, params)` 通过 MQTT 发布到 `unitree/go2/cmd`。 + - 运动相关:`custom_action`、`ContinuousMove`、`move`、`euler`、`set_speed_level`、`pose` 等。 + - 避障相关:`obstacle_avoidance_switch`、`obstacle_move`。 + - 导航相关:`start_mapping`、`end_mapping`、`initialize_pose`、`pose_navigation`、`pause_navigation`、`resume_navigation` 等(部分在工具层已注释)。 + - 充电:`start_recharge`、`stop_recharge`。 + - 服务管理:`list_services`、`switch_service`、`set_report_freq`。 + - `serve()` 中: + - 注册工具列表(Tool 定义),描述参数与取值范围。 + - 在 `call_tool` 中根据 `name` 分发到对应协程函数。 + - `mcp_mqtt.py`: + - 和 Temi 类似,管理 MQTT 客户端。 +- 配置要点: + - MQTT 连接参数在 `mcp_mqtt.py` 或 `.env` 中通过环境变量读取(具体字段请在接手时查看该文件)。 + +### 4.7 Temi 机器人 MCP(旧版,`lzwcai_temi_mcp`) + +- 入口: + - 包:`lzwcai_temi_mcp` + - 入口函数:`lzwcai_temi_mcp.main:main` + - MCP Server 名称:`lzwcai_temi_mcp` +- 核心文件: + - `main.py`: + - 当前暴露工具包含 `recharge`、`terminate`、`goto`、`speak`、`reception`、`notification`、`repose`、`patrol`、`dance`。 + - `mcp_mqtt.py`: + - 管理 MQTT 连接与消息发布。 + - `nav_server.py`: + - 封装导航与动作编排逻辑。 +- 维护建议: + - 仓库内同时存在 `terminal_temi_mcp` 与 `lzwcai_temi_mcp` 两套 Temi 服务,接手时建议先和业务方确认实际在线使用的是哪一套,避免重复维护。 + +## 五、安装与运行方式(面向开发/调试) + +以下步骤均建议在虚拟环境中执行。 + +1. 克隆仓库: + + ```bash + git clone + cd lzwcai-mcp + ``` + +2. 进入某个子项目目录并安装: + + ```bash + cd terminal_dhr_mcp + pip install -e .[dev] + # 或仅运行所需: + pip install -e . + ``` + + 其他子项目类似: + + - `cd lzwcai_lark_mcp && pip install -e .` + - `cd file_tools && pip install -e .` + - `cd k3cloud_mcp && pip install -e .` + - `cd lzwcai_temi_mcp && pip install -e .` + - `cd terminal_temi_mcp && pip install -e .` + - `cd terminal_go2_mcp && pip install -e .` + +3. 启动某个 MCP 服务器(本地调试): + + ```bash + # Lark + lzwcai-lark-mcp + + # 文件工具 + lzwcai-file-tools-mcp + + # 金蝶云星空 + k3cloud-mcp + + # 访客系统 + 门禁 + terminal-dhr-mcp + + # Temi 机器人(旧版) + lzwcai_temi_mcp + + # Temi 机器人 + terminal_temi_mcp + + # Go2 机器人 + terminal_go2_mcp + ``` + +4. 与 MCP Agent 集成: + + - 顶层或子项目中提供示例配置文件,如: + - `lzwcai_lark_mcp/mcp-server.json` + - `terminal_dhr_mcp/mcp-server-dhr.json` + - `terminal_temi_mcp/mcp-server-temi.json` + - `terminal_go2_mcp/mcp-server-go2.json` + - `lzwcai_temi_mcp/mcp-server.json` + - `file_tools/mcp-server-file-tools.json` + - `k3cloud_mcp/mcp-server.json` + - 在上层 Agent 的配置(例如 IDE 插件或自研 Agent)中引入这些 JSON 即可通过 `stdio` 调起对应服务器。 + +## 六、配置与敏感信息管理 + +- 所有敏感信息(token、密钥、账号密码)均通过环境变量或 `.env` 文件注入,不应直接写入代码或仓库。 +- 当前仓库中的部分 `mcp-server*.json` 已包含真实或准生产敏感配置示例;对外共享仓库前建议先做脱敏或改成占位符。 +- 推荐做法: + - 针对每个子项目,在其根目录创建 `.env`,仅存放本项目相关配置。 + - 在部署环境中,通过进程管理器(如 systemd、Docker、K8s)把 `.env` 或环境变量挂载进去。 +- 接手时建议重点确认: + - Lark 应用的 `app_id`/`app_secret` 是否有效,是否具备所需 API 权限。 + - K3Cloud 的 `BASE_URL`、账套、应用标识、用户名、密钥是否仍然有效,签名规则是否与现网一致。 + - 访客系统 / 门禁接口 `BASE_URL`、企业 ID、门禁实体 ID 是否与现网一致。 + - MQTT Broker 地址、账号密码是否与机器人当前网络环境匹配。 + - MinIO 的 endpoint、access key、secret key、bucket 名称是否存在且有权限。 + +## 七、开发规范与注意事项 + +- 编码风格: + - Python 代码整体接近 PEP 8 风格。 + - 部分子项目(如 `terminal_dhr_mcp`)在 `pyproject.toml` 中配置了 `black`、`isort`、`ruff`、`mypy` 等工具,对应规则可作为编码参考。 +- 日志: + - 所有服务器都使用 `logging.basicConfig` 统一输出日志,日志级别可通过 `LOG_LEVEL` 环境变量控制。 + - 出错时通常会在日志中打印详细异常,并向 MCP Agent 返回友好文本。 +- 错误处理: + - 多数工具会对必需参数做显式校验,缺失时抛出 `ValueError` 或返回带错误信息的字符串。 + - 对外 HTTP/MQTT 调用失败时,会记录错误日志,并抛出异常或返回失败说明。 +- 工具定义: + - 工具使用 `mcp.types.Tool` 定义,`inputSchema` 为 JSON Schema 格式,方便上层 Agent 自动生成表单或进行参数校验。 + - 若新增工具,请保持命名风格一致,并且在 `list_tools` 与 `call_tool` 实现中同步更新。 + +## 八、后续扩展建议 + +- 如需扩展新的硬件或业务系统,建议: + - 新增独立子项目目录(与现有结构一致),保持「一个 MCP 服务一个 Python 包」的模式。 + - 优先复用现有 MQTT/HTTP 封装模式与错误处理风格。 + - 在子项目内补充 README,简要说明工具列表与配置方式,并在顶层 `README.md` 中更新项目清单。 + +## 九、已知问题与接手优先项 + +- `file_tools/config.py` 默认从 `mcp-server-file-tools.json` 读取配置时,查找的 server key 是 `lzwcai-mcpskills-file-tools-mcp`,而当前示例文件使用的是 `lzwcai-file-tools-mcp`,可能导致本地自动注入环境变量失效。 +- `terminal_temi_mcp` 与 `lzwcai_temi_mcp` 为并行存在的两套 Temi MCP 服务,能力与工具名不完全一致,建议先统一服务边界再继续演进。 + +如在接手过程中遇到不清楚的业务规则(例如具体资产变动业务、访客流程、机器人业务场景),建议先与业务方确认需求,再在对应子项目中补充或调整工具逻辑。