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:
+5
-193
@@ -1,197 +1,9 @@
|
||||
# Business Facts
|
||||
# Order service extraction
|
||||
|
||||
Business facts are structured operational data used for exact counts, sums, and rankings. They complement the markdown wiki; they do not replace it.
|
||||
Order MCP and Kingdee synchronization now live in the independent project at `/home/nac/repo/kingdee-orders-mcp`. Its README documents CLI operations, MCP configuration, and systemd services. Neither project imports the other.
|
||||
|
||||
## Purpose
|
||||
The new project owns `.runtime/data/business-facts.sqlite`, private API credentials, token cache, and its daily incremental sync. MCP clients retain the existing URL and bearer token. The order tool names are unchanged. Wiki query, file lookup, and ingest are not exposed by the order server.
|
||||
|
||||
Use `<vault>/_meta/business-facts.sqlite` for questions where markdown is the wrong source of truth:
|
||||
The former `obsidian-wiki-mcp.service` and `obsidian-wiki-business-sync.timer` are disabled. The old vault `_meta/business-facts.sqlite` and private runtime configuration are retained as rollback backups, not active data sources. Do not run the old sync against this backup.
|
||||
|
||||
- how many sales orders were received in a month
|
||||
- monthly sales amount
|
||||
- customer, product, salesperson, organization, department, project, warehouse, status, and currency rankings or breakdowns
|
||||
- customer, product, and salesperson summaries
|
||||
- sales order counts, lists, trends, quantities, averages, and delivery-plan quantities
|
||||
|
||||
The wiki remains the place for context: customer background, project history, sales notes, email-derived signals, and cross-linked narrative knowledge.
|
||||
|
||||
## Independent Business MCP
|
||||
|
||||
The `business` MCP tool group queries the synchronized SQLite database directly.
|
||||
It does not start OpenCode, load `wiki-query`, read Wiki pages, or run Kingdee sync.
|
||||
Clients interpret natural-language requests and call these tools:
|
||||
|
||||
| Tool | Purpose |
|
||||
| --- | --- |
|
||||
| `get_business_schema` | Tables, templates, parameters, configured internal-party defaults, and counting rules |
|
||||
| `query_business_template` | Existing order, sales, customer/product/staff, and delivery-plan templates |
|
||||
| `execute_business_sql` | Guarded SELECT fallback when no template fits |
|
||||
| `get_business_status` | Data availability, stored order date range, and last successful sync |
|
||||
|
||||
To keep orders available while turning off all Wiki MCP capabilities:
|
||||
|
||||
```bash
|
||||
scripts/mcp_service.sh tools set business
|
||||
```
|
||||
|
||||
This saves `OBSIDIAN_MCP_ENABLED_TOOLS=business` and restarts the managed service
|
||||
once, preserving the connection URL and bearer token. Reconnect clients to refresh
|
||||
their tool list. `ask_wiki`, its job tools, file tools/resources, and ingest tools
|
||||
and upload routes are absent. `get_mcp_capabilities` remains available.
|
||||
|
||||
Example tool arguments for `query_business_template`:
|
||||
|
||||
```json
|
||||
{"template_name": "sales_amount", "params": {"year": 2026, "month": 6}}
|
||||
```
|
||||
|
||||
Database connections use SQLite `mode=ro` and `query_only`; queries never initialize
|
||||
or migrate the sync-owned database. Missing data returns an unavailable/error
|
||||
response rather than silently creating an empty database. The existing independent
|
||||
Kingdee timer continues to own updates. Database location defaults to
|
||||
`<vault>/_meta/business-facts.sqlite`; `--business-db` or
|
||||
`OBSIDIAN_MCP_BUSINESS_DB` (in the project `.env` or process environment) can select
|
||||
another existing database without changing the file-asset index.
|
||||
|
||||
Templates load the operator's `OBSIDIAN_BUSINESS_INTERNAL_PARTIES`,
|
||||
`OBSIDIAN_BUSINESS_INTERNAL_PARTY_IDS`, and `OBSIDIAN_BUSINESS_INCLUDE_INTERNAL_ORDERS`
|
||||
settings. Clients cannot replace the internal-party lists; `include_internal_orders`
|
||||
remains an explicit template option for internal-transfer audits. Guarded SQL keeps
|
||||
the existing table/column restrictions and row limits, and must express the internal
|
||||
transfer filter itself. All query results include `last_sync`, `is_stale`, and warnings.
|
||||
|
||||
The legacy `query` group still supports the combined OpenCode workflow when enabled.
|
||||
For example, `scripts/mcp_service.sh tools set business file-assets query ingest`
|
||||
enables both paths again. The groups are independently gated; this does not migrate
|
||||
or duplicate the business database.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
scripts/obsidian-wiki-ops.py kingdee sync --input ./kingdee-orders.json
|
||||
|
||||
scripts/obsidian-wiki-ops.py kingdee config
|
||||
# Fill .runtime/kingdee.env, then test the login/refresh flow:
|
||||
scripts/obsidian-wiki-ops.py kingdee auth-test
|
||||
|
||||
scripts/obsidian-wiki-ops.py kingdee sync \
|
||||
--start-bizdate 2026-06-01 \
|
||||
--end-bizdate 2026-06-30 \
|
||||
--billstatus C
|
||||
|
||||
scripts/obsidian-wiki-ops.py kingdee sync --start-modifytime "2026-07-01 00:00:00"
|
||||
scripts/obsidian-wiki-ops.py kingdee sync --incremental
|
||||
|
||||
# wiki-query uses this schema/template catalog to answer business questions.
|
||||
scripts/obsidian-wiki-ops.py kingdee schema --json
|
||||
scripts/obsidian-wiki-ops.py kingdee template order_count --vault /path/to/vault --year 2026 --month 6 --json
|
||||
scripts/obsidian-wiki-ops.py kingdee template sales_amount --vault /path/to/vault --year 2026 --month 6 --json
|
||||
scripts/obsidian-wiki-ops.py kingdee template top_products --vault /path/to/vault --year 2026 --month 6 --limit 10 --json
|
||||
scripts/obsidian-wiki-ops.py kingdee template staff_summary --vault /path/to/vault --start-date 2026-06-30 --end-date 2026-07-07 --staff 徐丽 --staff-role-scope all --json
|
||||
|
||||
# Guarded SQL is for gaps where no template fits.
|
||||
scripts/obsidian-wiki-ops.py kingdee sql "SELECT COUNT(*) AS order_count FROM orders"
|
||||
```
|
||||
|
||||
When `--input` is omitted, sync reads `.runtime/kingdee.env`, gets or refreshes an access token, calls the official Kingdee sales order API, and stores the returned rows. If the file does not exist, `business sync kingdee` creates the template and asks you to fill it before retrying.
|
||||
|
||||
For unattended sync, install the user-level systemd timer:
|
||||
|
||||
```bash
|
||||
scripts/obsidian-wiki-ops.py timer install --vault /path/to/vault
|
||||
scripts/obsidian-wiki-ops.py timer run-now --vault /path/to/vault
|
||||
scripts/obsidian-wiki-ops.py timer status
|
||||
```
|
||||
|
||||
The timer defaults to daily `07:10` with a 15-minute randomized delay. It runs incremental Kingdee sync with a 48-hour lookback window.
|
||||
|
||||
The config template includes:
|
||||
|
||||
- Token defaults follow Kingdee's enhanced OpenAPI token authentication doc: `https://vip.kingdee.com/knowledge/specialDetail/226337046514476288?category=239331354741842688&id=489812471545485056&productLineId=29`.
|
||||
- `KINGDEE_SALORDER_API_URL`: defaults to the official `querySalOrder` endpoint.
|
||||
- `KINGDEE_TOKEN_URL`: defaults to the official enhanced-token endpoint `/kapi/oauth2/getToken`.
|
||||
- `KINGDEE_CLIENT_ID`, `KINGDEE_CLIENT_SECRET`, `KINGDEE_USERNAME`, `KINGDEE_ACCOUNT_ID`, `KINGDEE_LANGUAGE`: fixed fields used to build the official getToken request body.
|
||||
- `nonce` and `timestamp`: generated automatically for every token request; `timestamp` uses the official zero-time-zone format by default.
|
||||
- `KINGDEE_TOKEN_BODY_EXTRA_JSON`: optional fixed extra fields merged into the generated token request body.
|
||||
- `KINGDEE_TOKEN_BODY_JSON`: advanced override for nonstandard token request bodies.
|
||||
- `KINGDEE_TOKEN_HEADERS_JSON`: optional token request headers, such as app-specific headers.
|
||||
- `KINGDEE_TOKEN_FIELD`, `KINGDEE_TOKEN_EXPIRES_IN_FIELD`, `KINGDEE_TOKEN_EXPIRES_IN_UNIT`, `KINGDEE_REFRESH_TOKEN_FIELD`: dot paths and units used to read token response fields. The official `expires_in` unit is milliseconds.
|
||||
- `KINGDEE_REFRESH_URL` and `KINGDEE_REFRESH_BODY_JSON`: optional refresh request. Use `{refresh_token}` in the JSON body template.
|
||||
- `KINGDEE_ACGW_IDENTITY`: value sent as the required `x-acgw-identity` sales-order API header.
|
||||
- `KINGDEE_PAGE_SIZE`: default sales-order page size.
|
||||
|
||||
`KINGDEE_LOGIN_URL`, `KINGDEE_LOGIN_BODY_JSON`, and `KINGDEE_LOGIN_HEADERS_JSON` are accepted as aliases for the token settings. `KINGDEE_ACCESS_TOKEN` can also be set for one-off manual testing. Fetched tokens are cached in `.runtime/kingdee-token.json`; both local files are ignored by git and should not be copied into `.env.example`.
|
||||
|
||||
The API response can still be imported from a JSON file for replay or testing. JSON may be a list of order objects or an object containing `orders`, `rows`, `data`, `records`, or `items`.
|
||||
|
||||
## Database
|
||||
|
||||
Default path: `<vault>/_meta/business-facts.sqlite`.
|
||||
|
||||
Tables:
|
||||
|
||||
- `sync_runs`: one row per Kingdee sync attempt, including status, cursor, record counts, and error text.
|
||||
- `raw_snapshots`: content-hashed raw JSON payloads for audit and replay.
|
||||
- `orders`: one normalized row per sales order. It keeps the query-critical fields (`order_id`, `order_no`, `order_date`, customer, status, organization, department, operator, currency, total amount, net amount, tax amount, address, comment) plus `raw_json` for the full original order header.
|
||||
- `order_items`: normalized material detail lines. It keeps material number/name/model, quantity, unit, price, tax-inclusive price, net amount, tax-inclusive amount, tax amount, delivery date, project, warehouse, and `raw_json` for the full original line.
|
||||
- `order_deliveries`: normalized delivery-plan rows from `billentry[].orderdeliverentry[]`. It keeps planned demand date, planned delivery date, planned quantity, planned base quantity, unit, receive address/place, remark, transport lead time, and `raw_json` for the full original delivery-plan row.
|
||||
- `order_staff`: sales staff roles extracted from the order header. Roles are `primary_sales`, `sales_2`, `sales_3`, `sales_4`, and `followup`.
|
||||
- `sync_state`: saved Kingdee incremental cursor.
|
||||
- `customers`: customer lookup data observed from orders.
|
||||
- `products`: product lookup data observed from order lines.
|
||||
|
||||
Order upsert key: `order_id`, falling back to `order_no` when no explicit ID is present. Re-syncing the same order updates the existing row and replaces that order's detail lines and delivery-plan rows.
|
||||
|
||||
`total_amount` is the statistic field used by monthly sales queries. For Kingdee sales orders it prefers `totalallamount` (价税合计) and falls back to other total fields only when the tax-inclusive total is absent. `total_net_amount`, `total_tax_amount`, and `total_all_amount` are stored separately so later reports can choose a different accounting basis without re-fetching historical payloads.
|
||||
|
||||
## Query Flow And Counting Basis
|
||||
|
||||
`wiki-query` decides semantically whether a user question needs business facts. When it does, it loads `obsidian-wiki business schema --json`, chooses a template if one fits, and only then uses guarded SQL for gaps. For hybrid questions about customers, products, salespeople, projects, delivery risk, anomalies, rankings, or quality impact, it runs two tracks: ordinary wiki retrieval for context/citations and business facts retrieval for structured metrics, then fuses the results in one answer.
|
||||
|
||||
For multi-company sales architectures, configure `OBSIDIAN_BUSINESS_INTERNAL_PARTIES` and/or `OBSIDIAN_BUSINESS_INTERNAL_PARTY_IDS` in `.env`. Templates then exclude intercompany transfer orders by default: an order is excluded only when both the sales organization and the ordering customer match configured internal parties. This keeps true external orders placed directly with a factory, while removing internal flow such as a sales company ordering from a factory. Set `OBSIDIAN_BUSINESS_INCLUDE_INTERNAL_ORDERS=true` or pass `--include-internal-orders` only when auditing internal transfers.
|
||||
|
||||
Template families include:
|
||||
|
||||
- Order totals: `order_count`, `sales_amount`, `customer_count`, `product_count`, `quantity_sum`, `avg_order_amount`.
|
||||
- Trends: `order_trend`, `sales_trend`, `quantity_trend`, grouped by day or month.
|
||||
- Rankings and breakdowns: customers, products, staff, organizations, departments, projects, warehouses, statuses, currencies.
|
||||
- Lists: orders, customer orders, product orders, staff orders.
|
||||
- Summaries: customer, product, and staff summaries.
|
||||
- Delivery plans: delivery-plan count, quantity, and list by planned date.
|
||||
|
||||
Core counting basis:
|
||||
|
||||
- Order count: `COUNT(DISTINCT order_no)` by `orders.order_date`.
|
||||
- Sales amount: `SUM(orders.total_amount)` by `orders.order_date`.
|
||||
- Ordered quantity: `SUM(order_items.quantity)` by `orders.order_date`.
|
||||
- Delivery-plan quantity: `SUM(order_deliveries.plan_qty)` by `order_deliveries.plan_date`.
|
||||
- Staff templates join `orders` to `order_staff`, defaulting to `primary_sales`.
|
||||
- When internal parties are configured, all built-in templates apply the intercompany dedupe filter before counting, summing, ranking, listing, or joining staff/delivery rows.
|
||||
|
||||
Every answer includes the statistic, counting basis, data source, and last successful sync time. If the last successful sync is before the query date, the answer warns that newer data may be missing.
|
||||
|
||||
Salesperson templates default to the main Kingdee salesperson (`operator_operatorname`). Use `--staff-role-scope all`, or wording such as "全部角色" or "含跟单", to include sales 2/3/4 and follow-up roles.
|
||||
|
||||
If no template matches, `wiki-query` may generate guarded SQL and execute it through `business sql`. The SQL fallback is still restricted:
|
||||
|
||||
- only one `SELECT` or `WITH ... SELECT` statement
|
||||
- only structured tables: `orders`, `order_items`, `order_deliveries`, `order_staff`, `customers`, `products`, `sync_runs`
|
||||
- no `raw_json` columns and no `raw_snapshots`
|
||||
- no write, schema, attach, pragma, or vacuum statements
|
||||
- default result limit is 100 rows, capped at 1000
|
||||
|
||||
When using guarded SQL for normal customer-facing sales metrics, apply the same intercompany dedupe rule manually unless the question is explicitly about internal transfer flow.
|
||||
|
||||
## Incremental Sync
|
||||
|
||||
`scripts/obsidian-wiki-ops.py kingdee sync --incremental` uses Kingdee `start_modifytime`.
|
||||
|
||||
- First run without a cursor: fetches the last 7 days.
|
||||
- Later runs: starts from the saved cursor minus 48 hours.
|
||||
- Explicit `--start-modifytime` or `--since` wins over the computed cursor.
|
||||
|
||||
The 48-hour lookback intentionally re-fetches recent orders so late edits and boundary cases are updated rather than missed. Upserts keep the database deduplicated by `order_id`.
|
||||
|
||||
## Boundary With Wiki Ingest
|
||||
|
||||
Do not create one markdown page per order or order line. That makes the wiki noisy and produces unreliable statistics.
|
||||
The database remains the fact source for exact numbers; the wiki remains the context source.
|
||||
For rollback, stop the new MCP and sync timer first. Restore Wiki code from commit `6c451c3` (prefer an isolated checkout), restore its private service environment from the migration backup, and point the legacy service at a consistent backup of the latest new database before enabling it. Do not resume the stale original database silently. Ensure only one sync owns the live database. New credentials and databases must never be committed to Git.
|
||||
|
||||
Reference in New Issue
Block a user