refactor(mcp): extract order services into independent project
Remove Kingdee sync, business CLI/MCP tools, and internal order-query routing now owned by the separate kingdee-orders-mcp project. Keep Wiki retrieval, file assets and ingest available in code while its deployed service is off. Retain legacy response fields with business access disabled, and document migration ownership and rollback without exposing credentials or data. Validation: 174 Wiki tests pass. Independent order service verified with 36 template parity checks, authenticated HTTP queries, and a real sync.
This commit is contained in:
@@ -18,18 +18,6 @@ from pathlib import Path
|
||||
|
||||
from obsidian_wiki import __version__
|
||||
from obsidian_wiki.access_control import audit_profile, export_profile, list_profiles
|
||||
from obsidian_wiki.business_facts import (
|
||||
business_schema,
|
||||
business_db_path_for_vault,
|
||||
compute_incremental_start_modifytime,
|
||||
execute_business_sql,
|
||||
execute_business_template,
|
||||
fetch_kingdee_payload,
|
||||
load_kingdee_payload_from_json,
|
||||
query_business_question,
|
||||
sync_kingdee_orders,
|
||||
)
|
||||
from obsidian_wiki.config import load_config
|
||||
from obsidian_wiki.file_assets import (
|
||||
clean_config_value,
|
||||
db_path_for_vault,
|
||||
@@ -39,13 +27,6 @@ from obsidian_wiki.file_assets import (
|
||||
search_assets,
|
||||
stage_files,
|
||||
)
|
||||
from obsidian_wiki.kingdee_api import (
|
||||
build_sales_order_filters,
|
||||
ensure_kingdee_config,
|
||||
get_kingdee_access_token,
|
||||
load_kingdee_config,
|
||||
token_cache_path_for_config,
|
||||
)
|
||||
|
||||
HOME = Path.home()
|
||||
GLOBAL_CONFIG_DIR = HOME / ".obsidian-wiki"
|
||||
@@ -438,20 +419,6 @@ def _resolve_file_asset_db(args: argparse.Namespace, vault_path: str) -> Path:
|
||||
return db_path
|
||||
|
||||
|
||||
def _resolve_business_db(args: argparse.Namespace, vault_path: str) -> Path:
|
||||
raw = (
|
||||
getattr(args, "db", None)
|
||||
or os.environ.get("OBSIDIAN_BUSINESS_FACTS_DB")
|
||||
or _read_config_value("OBSIDIAN_BUSINESS_FACTS_DB")
|
||||
)
|
||||
if not raw:
|
||||
return business_db_path_for_vault(vault_path)
|
||||
db_path = Path(clean_config_value(raw)).expanduser()
|
||||
if not db_path.is_absolute():
|
||||
db_path = Path(vault_path).expanduser() / db_path
|
||||
return db_path
|
||||
|
||||
|
||||
def cmd_file_index_init(args: argparse.Namespace) -> int:
|
||||
vault_path = _require_vault(args)
|
||||
db_path = _resolve_file_asset_db(args, vault_path)
|
||||
@@ -513,206 +480,6 @@ def cmd_file_index_stage(args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_business_sync_kingdee(args: argparse.Namespace) -> int:
|
||||
vault_path = _require_vault(args)
|
||||
db_path = _resolve_business_db(args, vault_path)
|
||||
filters: dict[str, object] = {}
|
||||
cursor_from = args.since
|
||||
if args.input:
|
||||
records, payload = load_kingdee_payload_from_json(args.input)
|
||||
else:
|
||||
config_path = getattr(args, "config", None)
|
||||
created = ensure_kingdee_config(config_path)
|
||||
overrides = _kingdee_cli_overrides(args)
|
||||
config = load_kingdee_config(config_path)
|
||||
config.update(overrides)
|
||||
has_official_token_credentials = all(
|
||||
config.get(key) for key in ("KINGDEE_CLIENT_ID", "KINGDEE_CLIENT_SECRET", "KINGDEE_USERNAME", "KINGDEE_ACCOUNT_ID")
|
||||
)
|
||||
if created and not (config.get("KINGDEE_ACCESS_TOKEN") or config.get("KINGDEE_TOKEN_BODY_JSON") or has_official_token_credentials):
|
||||
raise RuntimeError(f"Kingdee config template created at {Path(config_path or '.runtime/kingdee.env').expanduser()}; fill it and rerun")
|
||||
if args.dry_run_auth:
|
||||
token = get_kingdee_access_token(config, cache_path=token_cache_path_for_config(config_path))
|
||||
print(f"✅ Kingdee auth OK: token length {len(token)}")
|
||||
return 0
|
||||
filters = build_sales_order_filters(args)
|
||||
if getattr(args, "incremental", False) and not filters.get("start_modifytime"):
|
||||
filters["start_modifytime"] = compute_incremental_start_modifytime(
|
||||
db_path,
|
||||
lookback_hours=args.lookback_hours,
|
||||
initial_days=args.initial_days,
|
||||
)
|
||||
cursor_from = str(filters.get("start_modifytime") or args.since or "")
|
||||
records, payload = fetch_kingdee_payload(
|
||||
api_url=args.api_url,
|
||||
token=args.token or args.access_token,
|
||||
since=args.since,
|
||||
config_path=config_path,
|
||||
config_overrides=overrides,
|
||||
filters=filters,
|
||||
page_size=args.page_size,
|
||||
max_pages=args.max_pages,
|
||||
)
|
||||
result = sync_kingdee_orders(
|
||||
vault_path,
|
||||
records,
|
||||
source_payload=payload,
|
||||
db_path=db_path,
|
||||
cursor_from=cursor_from,
|
||||
)
|
||||
result["filters"] = filters
|
||||
if args.json:
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(f"✅ Synced Kingdee orders: {result['records_upserted']} record(s)")
|
||||
print(f" DB: {result['db_path']}")
|
||||
print(f" Last sync: {result['synced_at']}")
|
||||
if filters:
|
||||
print(f" Filters: {json.dumps(filters, ensure_ascii=False, sort_keys=True)}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_business_config_kingdee(args: argparse.Namespace) -> int:
|
||||
config_path = Path(args.config or ".runtime/kingdee.env").expanduser()
|
||||
created = ensure_kingdee_config(config_path)
|
||||
state = "created" if created else "exists"
|
||||
print(f"✅ Kingdee config {state}: {config_path}")
|
||||
return 0
|
||||
|
||||
|
||||
def _kingdee_cli_overrides(args: argparse.Namespace) -> dict[str, str]:
|
||||
overrides: dict[str, str] = {}
|
||||
if getattr(args, "api_url", None):
|
||||
overrides["KINGDEE_SALORDER_API_URL"] = args.api_url
|
||||
if getattr(args, "token", None):
|
||||
overrides["KINGDEE_ACCESS_TOKEN"] = args.token
|
||||
if getattr(args, "access_token", None):
|
||||
overrides["KINGDEE_ACCESS_TOKEN"] = args.access_token
|
||||
if getattr(args, "identity", None):
|
||||
overrides["KINGDEE_ACGW_IDENTITY"] = args.identity
|
||||
if getattr(args, "page_size", None):
|
||||
overrides["KINGDEE_PAGE_SIZE"] = str(args.page_size)
|
||||
return overrides
|
||||
|
||||
|
||||
def cmd_business_query(args: argparse.Namespace) -> int:
|
||||
vault_path = _require_vault(args)
|
||||
question = " ".join(args.question).strip()
|
||||
result = query_business_question(
|
||||
vault_path,
|
||||
question,
|
||||
db_path=_resolve_business_db(args, vault_path),
|
||||
all_staff=args.all_staff,
|
||||
fallback_sql=args.fallback_sql,
|
||||
business_params=_business_internal_params(args),
|
||||
)
|
||||
if args.json:
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(result["answer"])
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_business_schema(args: argparse.Namespace) -> int:
|
||||
schema = business_schema()
|
||||
if args.json:
|
||||
print(json.dumps(schema, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print("Business facts schema")
|
||||
print("Tables:")
|
||||
for table in schema["tables"]:
|
||||
print(f" - {table['name']}: {', '.join(table['fields'])}")
|
||||
print("Templates:")
|
||||
for template in schema["templates"]:
|
||||
required = ", ".join(template["required_parameters"]) or "none"
|
||||
print(f" - {template['name']}: {template['description']} (required: {required})")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_business_template(args: argparse.Namespace) -> int:
|
||||
vault_path = _require_vault(args)
|
||||
params = _business_template_params(args)
|
||||
result = execute_business_template(
|
||||
_resolve_business_db(args, vault_path),
|
||||
args.template_name,
|
||||
**params,
|
||||
)
|
||||
if args.json:
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(result["answer"])
|
||||
for row in result.get("rows", []):
|
||||
print(json.dumps(row, ensure_ascii=False, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
def _business_template_params(args: argparse.Namespace) -> dict[str, object]:
|
||||
keys = (
|
||||
"year",
|
||||
"month",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"end_date_exclusive",
|
||||
"limit",
|
||||
"granularity",
|
||||
"staff_role_scope",
|
||||
"customer",
|
||||
"product",
|
||||
"staff",
|
||||
"org",
|
||||
"department",
|
||||
"project",
|
||||
"warehouse",
|
||||
"billstatus",
|
||||
"orderstatus",
|
||||
"closestatus",
|
||||
"currency",
|
||||
)
|
||||
params = {key: getattr(args, key) for key in keys if getattr(args, key, None) not in (None, "", [])}
|
||||
params.update({key: value for key, value in _business_internal_params(args).items() if key not in params})
|
||||
return params
|
||||
|
||||
|
||||
def _business_internal_params(args: argparse.Namespace) -> dict[str, object]:
|
||||
params: dict[str, object] = {}
|
||||
if getattr(args, "internal_parties", None):
|
||||
params["internal_parties"] = getattr(args, "internal_parties")
|
||||
if getattr(args, "internal_party_ids", None):
|
||||
params["internal_party_ids"] = getattr(args, "internal_party_ids")
|
||||
if getattr(args, "include_internal_orders", False):
|
||||
params["include_internal_orders"] = True
|
||||
config = load_config(Path.cwd())
|
||||
if not params.get("internal_parties"):
|
||||
parties = os.environ.get("OBSIDIAN_BUSINESS_INTERNAL_PARTIES") or config.get("OBSIDIAN_BUSINESS_INTERNAL_PARTIES")
|
||||
if parties:
|
||||
params["internal_parties"] = parties
|
||||
if not params.get("internal_party_ids"):
|
||||
party_ids = os.environ.get("OBSIDIAN_BUSINESS_INTERNAL_PARTY_IDS") or config.get("OBSIDIAN_BUSINESS_INTERNAL_PARTY_IDS")
|
||||
if party_ids:
|
||||
params["internal_party_ids"] = party_ids
|
||||
include_internal = os.environ.get("OBSIDIAN_BUSINESS_INCLUDE_INTERNAL_ORDERS") or config.get("OBSIDIAN_BUSINESS_INCLUDE_INTERNAL_ORDERS")
|
||||
if include_internal and not params.get("include_internal_orders"):
|
||||
params["include_internal_orders"] = include_internal
|
||||
return params
|
||||
|
||||
|
||||
def cmd_business_sql(args: argparse.Namespace) -> int:
|
||||
vault_path = _require_vault(args)
|
||||
sql = " ".join(args.sql).strip()
|
||||
result = execute_business_sql(
|
||||
_resolve_business_db(args, vault_path),
|
||||
sql,
|
||||
limit=args.limit,
|
||||
)
|
||||
if args.json:
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(result["answer"])
|
||||
for row in result["rows"]:
|
||||
print(json.dumps(row, ensure_ascii=False, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_mcp_server(args: argparse.Namespace) -> int:
|
||||
from obsidian_wiki.mcp_server import main as mcp_main
|
||||
|
||||
@@ -835,130 +602,6 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
ftp.add_argument("--visibility", default="internal", help="initial visibility")
|
||||
ftp.set_defaults(func=cmd_file_index_stage)
|
||||
|
||||
bp = sub.add_parser("business", help="sync and query structured business facts")
|
||||
business_sub = bp.add_subparsers(dest="business_command")
|
||||
|
||||
bcp = business_sub.add_parser("config", help="create local business integration config files")
|
||||
business_config_sub = bcp.add_subparsers(dest="business_config_target")
|
||||
|
||||
bckp = business_config_sub.add_parser("kingdee", help="create a local Kingdee API config template")
|
||||
bckp.add_argument("--config", metavar="PATH", help="Kingdee config path (defaults to .runtime/kingdee.env)")
|
||||
bckp.set_defaults(func=cmd_business_config_kingdee)
|
||||
|
||||
bsp = business_sub.add_parser("sync", help="sync business facts from an external system")
|
||||
business_sync_sub = bsp.add_subparsers(dest="business_sync_source")
|
||||
|
||||
bkp = business_sync_sub.add_parser("kingdee", help="sync Kingdee sales orders")
|
||||
_add_vault_arg(bkp)
|
||||
bkp.add_argument("--db", metavar="PATH", help="SQLite DB path (defaults to <vault>/_meta/business-facts.sqlite)")
|
||||
bkp.add_argument("--input", metavar="PATH", help="JSON payload exported from or fetched from Kingdee")
|
||||
bkp.add_argument("--config", metavar="PATH", help="Kingdee config path (defaults to .runtime/kingdee.env)")
|
||||
bkp.add_argument("--api-url", help="Kingdee sales order API URL; defaults to KINGDEE_SALORDER_API_URL")
|
||||
bkp.add_argument("--token", help="direct access token override; compatibility alias for --access-token")
|
||||
bkp.add_argument("--access-token", help="direct access token override")
|
||||
bkp.add_argument("--identity", help="x-acgw-identity override")
|
||||
bkp.add_argument("--since", help="optional updated_since cursor passed to the API and sync log")
|
||||
bkp.add_argument("--start-bizdate")
|
||||
bkp.add_argument("--end-bizdate")
|
||||
bkp.add_argument("--start-createtime")
|
||||
bkp.add_argument("--end-createtime")
|
||||
bkp.add_argument("--start-modifytime")
|
||||
bkp.add_argument("--end-modifytime")
|
||||
bkp.add_argument("--start-auditdate")
|
||||
bkp.add_argument("--end-auditdate")
|
||||
bkp.add_argument("--id", action="append", help="sales order ID; repeat or comma-separate")
|
||||
bkp.add_argument("--billno", action="append", help="sales order bill number; repeat or comma-separate")
|
||||
bkp.add_argument("--billstatus", action="append", help="A/B/C status; repeat or comma-separate")
|
||||
bkp.add_argument("--org-number", action="append", help="sales org code; repeat or comma-separate")
|
||||
bkp.add_argument("--customer-number", action="append", help="customer code; repeat or comma-separate")
|
||||
bkp.add_argument("--customer-createorg-number", action="append", help="customer create org code; repeat or comma-separate")
|
||||
bkp.add_argument("--page-size", type=int, help="Kingdee pageSize")
|
||||
bkp.add_argument("--max-pages", type=int, default=100, help="maximum pages to fetch")
|
||||
bkp.add_argument("--incremental", action="store_true", help="use saved modifytime cursor with a lookback window")
|
||||
bkp.add_argument("--lookback-hours", type=int, default=48, help="hours to subtract from the saved incremental cursor")
|
||||
bkp.add_argument("--initial-days", type=int, default=7, help="days to fetch on the first incremental sync")
|
||||
bkp.add_argument("--dry-run-auth", action="store_true", help="fetch/refresh access token and exit without syncing orders")
|
||||
bkp.add_argument("--json", action="store_true", help="print machine-readable sync result")
|
||||
bkp.set_defaults(func=cmd_business_sync_kingdee)
|
||||
|
||||
bqp = business_sub.add_parser("query", help="answer fixed-template business statistics questions")
|
||||
_add_vault_arg(bqp)
|
||||
bqp.add_argument("--db", metavar="PATH", help="SQLite DB path (defaults to <vault>/_meta/business-facts.sqlite)")
|
||||
bqp.add_argument("--all-staff", action="store_true", help="include secondary sales and follow-up roles in staff queries")
|
||||
bqp.add_argument("--fallback-sql", help="guarded read-only SQL to run only when no fixed template matches")
|
||||
bqp.add_argument(
|
||||
"--internal-party",
|
||||
dest="internal_parties",
|
||||
action="append",
|
||||
help="internal company name substring to exclude intercompany transfer orders; repeat or comma-separate",
|
||||
)
|
||||
bqp.add_argument(
|
||||
"--internal-party-id",
|
||||
dest="internal_party_ids",
|
||||
action="append",
|
||||
help="internal org/customer code to exclude intercompany transfer orders; repeat or comma-separate",
|
||||
)
|
||||
bqp.add_argument("--include-internal-orders", action="store_true", help="include configured internal transfer orders")
|
||||
bqp.add_argument("--json", action="store_true", help="print machine-readable query result")
|
||||
bqp.add_argument("question", nargs="+", help="question, e.g. '2026-06 order count'")
|
||||
bqp.set_defaults(func=cmd_business_query)
|
||||
|
||||
bsch = business_sub.add_parser("schema", help="print business fact tables, templates, and SQL guardrails")
|
||||
bsch.add_argument("--json", action="store_true", help="print machine-readable schema")
|
||||
bsch.set_defaults(func=cmd_business_schema)
|
||||
|
||||
btp = business_sub.add_parser("template", help="run one business query template with explicit parameters")
|
||||
_add_vault_arg(btp)
|
||||
btp.add_argument("--db", metavar="PATH", help="SQLite DB path (defaults to <vault>/_meta/business-facts.sqlite)")
|
||||
btp.add_argument("--year", type=int, help="calendar year, e.g. 2026")
|
||||
btp.add_argument("--month", type=int, help="calendar month, 1-12")
|
||||
btp.add_argument("--start-date", help="inclusive date, YYYY-MM-DD")
|
||||
btp.add_argument("--end-date", help="exclusive date, YYYY-MM-DD")
|
||||
btp.add_argument("--end-date-exclusive", help="explicit exclusive end date, YYYY-MM-DD")
|
||||
btp.add_argument("--limit", type=int, default=10, help="maximum rows for list/ranking templates")
|
||||
btp.add_argument("--granularity", choices=("day", "month"), default="month", help="trend grouping")
|
||||
btp.add_argument(
|
||||
"--staff-role-scope",
|
||||
choices=("primary", "all"),
|
||||
default="primary",
|
||||
help="primary sales only by default; all includes secondary sales and follow-up roles",
|
||||
)
|
||||
btp.add_argument("--customer", help="customer name or code filter")
|
||||
btp.add_argument("--product", help="product/material name, code, or model filter")
|
||||
btp.add_argument("--staff", help="staff name or code filter")
|
||||
btp.add_argument("--org", help="sales organization name or code filter")
|
||||
btp.add_argument("--department", help="sales department name or code filter")
|
||||
btp.add_argument("--project", help="project name or code filter")
|
||||
btp.add_argument("--warehouse", help="warehouse name or code filter")
|
||||
btp.add_argument("--billstatus", help="Kingdee billstatus filter")
|
||||
btp.add_argument("--orderstatus", help="Kingdee orderstatus filter")
|
||||
btp.add_argument("--closestatus", help="Kingdee closestatus filter")
|
||||
btp.add_argument("--currency", help="currency code filter")
|
||||
btp.add_argument(
|
||||
"--internal-party",
|
||||
dest="internal_parties",
|
||||
action="append",
|
||||
help="internal company name substring to exclude intercompany transfer orders; repeat or comma-separate",
|
||||
)
|
||||
btp.add_argument(
|
||||
"--internal-party-id",
|
||||
dest="internal_party_ids",
|
||||
action="append",
|
||||
help="internal org/customer code to exclude intercompany transfer orders; repeat or comma-separate",
|
||||
)
|
||||
btp.add_argument("--include-internal-orders", action="store_true", help="include configured internal transfer orders")
|
||||
btp.add_argument("--json", action="store_true", help="print machine-readable template result")
|
||||
btp.add_argument("template_name", help="template name from `business schema`")
|
||||
btp.set_defaults(func=cmd_business_template)
|
||||
|
||||
bsp_sql = business_sub.add_parser("sql", help="run guarded read-only SQL against structured business facts")
|
||||
_add_vault_arg(bsp_sql)
|
||||
bsp_sql.add_argument("--db", metavar="PATH", help="SQLite DB path (defaults to <vault>/_meta/business-facts.sqlite)")
|
||||
bsp_sql.add_argument("--limit", type=int, default=100, help="maximum rows to return, capped at 1000")
|
||||
bsp_sql.add_argument("--json", action="store_true", help="print machine-readable SQL result")
|
||||
bsp_sql.add_argument("sql", nargs="+", help="single SELECT or WITH ... SELECT statement")
|
||||
bsp_sql.set_defaults(func=cmd_business_sql)
|
||||
|
||||
mp = sub.add_parser("mcp-server", help="run the MCP server for safe source-file lookup")
|
||||
_add_file_index_storage_args(mp)
|
||||
mp.add_argument("--profile", help="bot profile name for ACL checks")
|
||||
|
||||
Reference in New Issue
Block a user