erp-integral-mcp 1.2.0__tar.gz

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.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: erp-integral-mcp
3
+ Version: 1.2.0
4
+ Summary: 鼎尖 ERP 业务动作积分统计 MCP:积分项查询、按日/按积分项汇总
5
+ License: Proprietary
6
+ Project-URL: Homepage, https://open.workbuddy.cn/docs/connector
7
+ Project-URL: Documentation, https://open.workbuddy.cn/docs/connector
8
+ Keywords: mcp,dingjian,erp,integral,settlement,stdio,uvx,workbuddy
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: mcp>=1.2.0
12
+ Requires-Dist: httpx>=0.27.0
13
+ Requires-Dist: pydantic>=2.0
14
+ Requires-Dist: pydantic-settings>=2.0
15
+ Requires-Dist: python-dotenv>=1.0.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8.0; extra == "dev"
18
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
19
+
20
+ # erp-integral-mcp
21
+
22
+ 鼎尖 ERP **业务动作积分**统计 MCP:第三方 SHA1 签名鉴权,查询积分项清单,并按**日历日**或**积分项**汇总每位员工的作业积分。
23
+
24
+ ## 接口
25
+
26
+ | Tool | ERP API | 说明 |
27
+ | --- | --- | --- |
28
+ | `search_persons` | `basedata/person/dataPickerData` | 姓名/工号 → personId(F7) |
29
+ | `list_integral_jobsets` | `settlement/integralDetail/getIntegralConditon` | 积分项(jobSet)清单 |
30
+ | `query_integral_stats` | `settlement/integralDetail/integralCountData` | 人员积分;支持 `personId`;`group_by=week` |
31
+
32
+ `columnType`:
33
+
34
+ - `INTEGRALDATE`:按日,`scoreN` = 当月第 N 日积分
35
+ - `INTEGRALJOBSET`:按积分项,字段名为积分项 `id`,需结合 `jobSetList` 解读
36
+
37
+ ## 环境变量
38
+
39
+ 与 `erp-property-mcp` 相同:
40
+
41
+ ```bash
42
+ ERP_BASE_URL=https://your-host/dingjian/
43
+ ERP_SECRET=...
44
+ ERP_CU_ID=...
45
+ ERP_USER_ID=...
46
+ ERP_DC=...
47
+ ```
48
+
49
+ ## 本地运行
50
+
51
+ ```bash
52
+ cd mcp/erp-integral-mcp
53
+ cp .env.example .env # 填入凭证
54
+ python -m venv .venv && source .venv/bin/activate
55
+ pip install -e ".[dev]"
56
+ pytest
57
+ erp-integral-mcp # stdio MCP
58
+ ```
59
+
60
+ ## uvx / WorkBuddy
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "erp_integral": {
66
+ "command": "uvx",
67
+ "args": ["erp-integral-mcp@1.0.0"],
68
+ "env": {
69
+ "ERP_BASE_URL": "${ERP_BASE_URL}",
70
+ "ERP_SECRET": "${ERP_SECRET}",
71
+ "ERP_CU_ID": "${ERP_CU_ID}",
72
+ "ERP_USER_ID": "${ERP_USER_ID}",
73
+ "ERP_DC": "${ERP_DC}"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ 连接器打包:
81
+
82
+ ```bash
83
+ ./scripts/pack-connector.sh
84
+ ```
85
+
86
+ ## Skill
87
+
88
+ 业务作业 SOP 见 `skills/erp-integral-stats/SKILL.md`(仓库同步副本:`mcp/skills/erp-integral-stats/SKILL.md`)。
@@ -0,0 +1,69 @@
1
+ # erp-integral-mcp
2
+
3
+ 鼎尖 ERP **业务动作积分**统计 MCP:第三方 SHA1 签名鉴权,查询积分项清单,并按**日历日**或**积分项**汇总每位员工的作业积分。
4
+
5
+ ## 接口
6
+
7
+ | Tool | ERP API | 说明 |
8
+ | --- | --- | --- |
9
+ | `search_persons` | `basedata/person/dataPickerData` | 姓名/工号 → personId(F7) |
10
+ | `list_integral_jobsets` | `settlement/integralDetail/getIntegralConditon` | 积分项(jobSet)清单 |
11
+ | `query_integral_stats` | `settlement/integralDetail/integralCountData` | 人员积分;支持 `personId`;`group_by=week` |
12
+
13
+ `columnType`:
14
+
15
+ - `INTEGRALDATE`:按日,`scoreN` = 当月第 N 日积分
16
+ - `INTEGRALJOBSET`:按积分项,字段名为积分项 `id`,需结合 `jobSetList` 解读
17
+
18
+ ## 环境变量
19
+
20
+ 与 `erp-property-mcp` 相同:
21
+
22
+ ```bash
23
+ ERP_BASE_URL=https://your-host/dingjian/
24
+ ERP_SECRET=...
25
+ ERP_CU_ID=...
26
+ ERP_USER_ID=...
27
+ ERP_DC=...
28
+ ```
29
+
30
+ ## 本地运行
31
+
32
+ ```bash
33
+ cd mcp/erp-integral-mcp
34
+ cp .env.example .env # 填入凭证
35
+ python -m venv .venv && source .venv/bin/activate
36
+ pip install -e ".[dev]"
37
+ pytest
38
+ erp-integral-mcp # stdio MCP
39
+ ```
40
+
41
+ ## uvx / WorkBuddy
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "erp_integral": {
47
+ "command": "uvx",
48
+ "args": ["erp-integral-mcp@1.0.0"],
49
+ "env": {
50
+ "ERP_BASE_URL": "${ERP_BASE_URL}",
51
+ "ERP_SECRET": "${ERP_SECRET}",
52
+ "ERP_CU_ID": "${ERP_CU_ID}",
53
+ "ERP_USER_ID": "${ERP_USER_ID}",
54
+ "ERP_DC": "${ERP_DC}"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ 连接器打包:
62
+
63
+ ```bash
64
+ ./scripts/pack-connector.sh
65
+ ```
66
+
67
+ ## Skill
68
+
69
+ 业务作业 SOP 见 `skills/erp-integral-stats/SKILL.md`(仓库同步副本:`mcp/skills/erp-integral-stats/SKILL.md`)。
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "erp-integral-mcp"
7
+ version = "1.2.0"
8
+ description = "鼎尖 ERP 业务动作积分统计 MCP:积分项查询、按日/按积分项汇总"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Proprietary" }
12
+ keywords = ["mcp", "dingjian", "erp", "integral", "settlement", "stdio", "uvx", "workbuddy"]
13
+ dependencies = [
14
+ "mcp>=1.2.0",
15
+ "httpx>=0.27.0",
16
+ "pydantic>=2.0",
17
+ "pydantic-settings>=2.0",
18
+ "python-dotenv>=1.0.0",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://open.workbuddy.cn/docs/connector"
23
+ Documentation = "https://open.workbuddy.cn/docs/connector"
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=8.0",
28
+ "pytest-asyncio>=0.24",
29
+ ]
30
+
31
+ [project.scripts]
32
+ erp-integral-mcp = "erp_integral_mcp.__main__:main"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [tool.pytest.ini_options]
38
+ asyncio_mode = "auto"
39
+ testpaths = ["tests"]
40
+ pythonpath = ["tests", "src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """鼎尖 ERP 业务动作积分统计 MCP。"""
2
+
3
+ __version__ = "1.2.0"
@@ -0,0 +1,9 @@
1
+ from erp_integral_mcp.server import run
2
+
3
+
4
+ def main() -> None:
5
+ run()
6
+
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -0,0 +1,59 @@
1
+ from functools import lru_cache
2
+ from pathlib import Path
3
+
4
+ from dotenv import load_dotenv
5
+ from pydantic import Field
6
+ from pydantic_settings import BaseSettings, SettingsConfigDict
7
+
8
+ from erp_integral_mcp.erp.auth import require_config
9
+
10
+ PACKAGE_ROOT = Path(__file__).resolve().parents[2]
11
+ ENV_FILE = PACKAGE_ROOT / ".env"
12
+
13
+ GET_CONDITION_API = "settlement/integralDetail/getIntegralConditon"
14
+ COUNT_DATA_API = "settlement/integralDetail/integralCountData"
15
+ PERSON_PICKER_API = "basedata/person/dataPickerData"
16
+ COLUMN_BY_DATE = "INTEGRALDATE"
17
+ COLUMN_BY_JOBSET = "INTEGRALJOBSET"
18
+
19
+ load_dotenv(ENV_FILE)
20
+
21
+
22
+ class Settings(BaseSettings):
23
+ model_config = SettingsConfigDict(
24
+ env_file=str(ENV_FILE),
25
+ env_file_encoding="utf-8",
26
+ extra="ignore",
27
+ )
28
+
29
+ erp_base_url: str = Field(alias="ERP_BASE_URL")
30
+ erp_secret: str = Field(alias="ERP_SECRET")
31
+ erp_cu_id: str = Field(alias="ERP_CU_ID")
32
+ erp_user_id: str = Field(alias="ERP_USER_ID")
33
+ erp_dc: str = Field(alias="ERP_DC")
34
+ erp_timeout_seconds: int = Field(default=60, alias="ERP_TIMEOUT_SECONDS")
35
+
36
+ @property
37
+ def base_url(self) -> str:
38
+ return self.erp_base_url.rstrip("/") + "/"
39
+
40
+ def validate_credentials(self) -> None:
41
+ require_config(
42
+ {
43
+ "ERP_SECRET": self.erp_secret,
44
+ "ERP_CU_ID": self.erp_cu_id,
45
+ "ERP_USER_ID": self.erp_user_id,
46
+ "ERP_DC": self.erp_dc,
47
+ },
48
+ [
49
+ "ERP_SECRET",
50
+ "ERP_CU_ID",
51
+ "ERP_USER_ID",
52
+ "ERP_DC",
53
+ ],
54
+ )
55
+
56
+
57
+ @lru_cache
58
+ def get_settings() -> Settings:
59
+ return Settings() # type: ignore[call-arg]
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+
5
+ from erp_integral_mcp.config import Settings, get_settings
6
+ from erp_integral_mcp.erp.client import ErpClient
7
+
8
+
9
+ class AppContext:
10
+ def __init__(self, settings: Settings) -> None:
11
+ self.settings = settings
12
+ self.client = ErpClient(settings)
13
+
14
+
15
+ @lru_cache
16
+ def get_context() -> AppContext:
17
+ return AppContext(get_settings())
@@ -0,0 +1 @@
1
+ """ERP 第三方接口客户端。"""
@@ -0,0 +1,65 @@
1
+ """鼎尖 ERP 第三方签名鉴权(对齐《第三方接口规范》Java 示例)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import time
7
+ from typing import Mapping
8
+
9
+
10
+ def build_signature(
11
+ *,
12
+ secret: str,
13
+ third_timestamp: str,
14
+ third_cu_id: str,
15
+ third_user_id: str,
16
+ third_dc: str,
17
+ api_path: str,
18
+ ) -> str:
19
+ """将 secret/timestamp/cuId/userId/dc/apiPath 字典序排序后拼接,再做 SHA1 hex。"""
20
+ parts = [
21
+ secret,
22
+ third_timestamp,
23
+ third_cu_id,
24
+ third_user_id,
25
+ third_dc,
26
+ api_path,
27
+ ]
28
+ parts.sort()
29
+ return hashlib.sha1("".join(parts).encode("utf-8")).hexdigest()
30
+
31
+
32
+ def build_auth_headers(
33
+ *,
34
+ secret: str,
35
+ third_cu_id: str,
36
+ third_user_id: str,
37
+ third_dc: str,
38
+ api_path: str,
39
+ timestamp_ms: str | None = None,
40
+ ) -> dict[str, str]:
41
+ """生成第三方开放接口所需请求头。"""
42
+ third_timestamp = timestamp_ms or str(int(time.time() * 1000))
43
+ signature = build_signature(
44
+ secret=secret,
45
+ third_timestamp=third_timestamp,
46
+ third_cu_id=third_cu_id,
47
+ third_user_id=third_user_id,
48
+ third_dc=third_dc,
49
+ api_path=api_path,
50
+ )
51
+ return {
52
+ "thirdtimestamp": third_timestamp,
53
+ "thirdCuId": third_cu_id,
54
+ "thirdUserId": third_user_id,
55
+ "thirdDc": third_dc,
56
+ "djthirdsign": signature,
57
+ "content-type": "application/json;charset=UTF-8",
58
+ "accept": "application/json, text/plain, */*",
59
+ }
60
+
61
+
62
+ def require_config(values: Mapping[str, str | None], keys: list[str]) -> None:
63
+ missing = [key for key in keys if not (values.get(key) or "").strip()]
64
+ if missing:
65
+ raise ValueError("缺少 ERP 第三方凭证,请配置环境变量: " + ", ".join(missing))
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Any
6
+ from urllib.parse import urljoin
7
+
8
+ import httpx
9
+
10
+ from erp_integral_mcp.config import (
11
+ COUNT_DATA_API,
12
+ GET_CONDITION_API,
13
+ PERSON_PICKER_API,
14
+ Settings,
15
+ )
16
+ from erp_integral_mcp.erp.auth import build_auth_headers
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class ErpClient:
22
+ """调用鼎尖 ERP 第三方开放接口(签名鉴权,无需 Bearer Token)。"""
23
+
24
+ def __init__(self, settings: Settings) -> None:
25
+ self._settings = settings
26
+ settings.validate_credentials()
27
+
28
+ def _url(self, api_path: str) -> str:
29
+ base = self._settings.base_url
30
+ return urljoin(base, f"{api_path}?djrqformat=pcjson")
31
+
32
+ async def _post(self, api_path: str, body: dict[str, Any]) -> dict[str, Any]:
33
+ last_error: Exception | None = None
34
+ for attempt in range(3):
35
+ headers = build_auth_headers(
36
+ secret=self._settings.erp_secret,
37
+ third_cu_id=self._settings.erp_cu_id,
38
+ third_user_id=self._settings.erp_user_id,
39
+ third_dc=self._settings.erp_dc,
40
+ api_path=api_path,
41
+ )
42
+ try:
43
+ async with httpx.AsyncClient(
44
+ timeout=self._settings.erp_timeout_seconds
45
+ ) as client:
46
+ resp = await client.post(
47
+ self._url(api_path),
48
+ headers=headers,
49
+ json=body,
50
+ )
51
+ if resp.status_code >= 400:
52
+ raise RuntimeError(
53
+ f"ERP HTTP {resp.status_code} {api_path}: {resp.text[:300]}"
54
+ )
55
+ data = resp.json()
56
+ except Exception as exc: # noqa: BLE001
57
+ last_error = exc
58
+ await asyncio.sleep(0.6 * (attempt + 1))
59
+ continue
60
+
61
+ if not isinstance(data, dict):
62
+ raise RuntimeError(f"ERP 返回非 JSON 对象: {type(data)}")
63
+
64
+ if data.get("resultType") in (903, "903"):
65
+ last_error = RuntimeError(str(data.get("resultMsg") or "QPS限流"))
66
+ await asyncio.sleep(0.8 * (attempt + 1))
67
+ continue
68
+
69
+ if data.get("resultType") not in (None, 1):
70
+ raise RuntimeError(
71
+ f"ERP 业务失败 resultType={data.get('resultType')} "
72
+ f"resultMsg={data.get('resultMsg')}"
73
+ )
74
+ return data
75
+
76
+ if last_error:
77
+ raise RuntimeError(f"ERP 请求失败: {last_error}") from last_error
78
+ raise RuntimeError("ERP 请求失败")
79
+
80
+ async def get_integral_condition(
81
+ self, *, search_type: str = "integralCount"
82
+ ) -> dict[str, Any]:
83
+ return await self._post(
84
+ GET_CONDITION_API,
85
+ {"searchType": search_type},
86
+ )
87
+
88
+ async def integral_count_data(self, body: dict[str, Any]) -> dict[str, Any]:
89
+ return await self._post(COUNT_DATA_API, body)
90
+
91
+ async def search_persons(self, body: dict[str, Any]) -> dict[str, Any]:
92
+ """人员 F7:basedata/person/dataPickerData(与积分页员工选择器同源)。"""
93
+ return await self._post(PERSON_PICKER_API, body)