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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user