feat(business): add Kingdee fact queries

Add a structured business facts SQLite layer for Kingdee sales orders, with fixed-template query commands for monthly order counts, sales amount, top customers, and customer trend data.

Update README, SETUP, and developer docs to describe configuration, sync commands, query examples, and the boundary between structured facts and wiki ingest.

Validation: python3 -m unittest discover -v
This commit is contained in:
Codex
2026-07-06 19:19:23 +00:00
parent 3d0203fb9e
commit 4dfef7e099
6 changed files with 904 additions and 0 deletions
+81
View File
@@ -18,6 +18,13 @@ 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_db_path_for_vault,
fetch_kingdee_payload,
load_kingdee_payload_from_json,
query_business_question,
sync_kingdee_orders,
)
from obsidian_wiki.file_assets import (
clean_config_value,
db_path_for_vault,
@@ -419,6 +426,20 @@ 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)
@@ -480,6 +501,43 @@ 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)
if args.input:
records, payload = load_kingdee_payload_from_json(args.input)
else:
records, payload = fetch_kingdee_payload(api_url=args.api_url, token=args.token, since=args.since)
result = sync_kingdee_orders(
vault_path,
records,
source_payload=payload,
db_path=_resolve_business_db(args, vault_path),
cursor_from=args.since,
)
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']}")
return 0
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),
)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(result["answer"])
return 0
def cmd_mcp_server(args: argparse.Namespace) -> int:
from obsidian_wiki.mcp_server import main as mcp_main
@@ -602,6 +660,29 @@ 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")
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("--api-url", help="Kingdee-compatible JSON API URL; defaults to KINGDEE_API_URL")
bkp.add_argument("--token", help="API bearer token; defaults to KINGDEE_API_TOKEN")
bkp.add_argument("--since", help="optional updated_since cursor passed to the API and sync log")
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("--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)
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")