feat(cli): add unified user operations entrypoint

Add scripts/obsidian-wiki-ops.py as the repo-local command surface for common wiki, Kingdee, timer, daily update, and MCP operations.

Route the existing business and MCP shell wrappers through the unified entrypoint for compatibility, and update docs to recommend the single command path.

Validation: python3 -m unittest discover -v; python3 -m compileall obsidian_wiki tests scripts; git diff --check.
This commit is contained in:
Codex
2026-07-06 21:21:14 +00:00
parent 317b0e6feb
commit b85c26e0ba
11 changed files with 306 additions and 71 deletions
+43 -34
View File
@@ -38,6 +38,15 @@ obsidian-wiki setup --project . # also drop project-local skills + AGENTS.md int
obsidian-wiki setup --copy # copy skill files instead of symlinking
```
When working from a cloned repo, `scripts/obsidian-wiki-ops.py` is the single repo-local operations entrypoint. It wraps common `obsidian-wiki` commands, Kingdee business sync/query, the business sync timer, daily update, and the managed MCP service:
```bash
scripts/obsidian-wiki-ops.py help
scripts/obsidian-wiki-ops.py kingdee sync --incremental
scripts/obsidian-wiki-ops.py timer install --vault /path/to/vault
scripts/obsidian-wiki-ops.py mcp start
```
`OBSIDIAN_VAULT_PATH` is just any directory where you want your wiki documents to live — a new empty folder or an existing Obsidian vault. Omit `--vault` to be prompted (or set it later in `~/.obsidian-wiki/config`).
### Install via Skills CLI
@@ -320,29 +329,29 @@ Operational counts and amounts belong in a structured fact database, not in mark
```bash
# Sync from an exported or API-fetched Kingdee JSON payload.
obsidian-wiki business sync kingdee --input ./kingdee-orders.json
scripts/obsidian-wiki-ops.py kingdee sync --input ./kingdee-orders.json
# Or create the private local config for the real Kingdee API/token flow.
obsidian-wiki business config kingdee
scripts/obsidian-wiki-ops.py kingdee config
vim .runtime/kingdee.env
obsidian-wiki business sync kingdee --dry-run-auth
scripts/obsidian-wiki-ops.py kingdee auth-test
# Then fetch reviewed June orders from Kingdee.
obsidian-wiki business sync kingdee \
scripts/obsidian-wiki-ops.py kingdee sync \
--start-bizdate 2026-06-01 \
--end-bizdate 2026-06-30 \
--billstatus C
# Or fetch incrementally by Kingdee modify time.
obsidian-wiki business sync kingdee --incremental
scripts/obsidian-wiki-ops.py kingdee sync --incremental
# Ask deterministic statistics questions.
obsidian-wiki business query "2026年6月接了多少单"
obsidian-wiki business query "2026-06 sales amount"
obsidian-wiki business query "2026-06 top customer"
obsidian-wiki business query "徐丽最近一周订单"
obsidian-wiki business query "徐丽最近一周销售额" --all-staff
obsidian-wiki business sql "SELECT COUNT(*) AS order_count FROM orders"
scripts/obsidian-wiki-ops.py kingdee query "2026年6月接了多少单"
scripts/obsidian-wiki-ops.py kingdee query "2026-06 sales amount"
scripts/obsidian-wiki-ops.py kingdee query "2026-06 top customer"
scripts/obsidian-wiki-ops.py kingdee query "徐丽最近一周订单"
scripts/obsidian-wiki-ops.py kingdee query "徐丽最近一周销售额" --all-staff
scripts/obsidian-wiki-ops.py kingdee sql "SELECT COUNT(*) AS order_count FROM orders"
```
V1 uses fixed templates for monthly order count, monthly sales amount, monthly top customers, customer trend, and salesperson order list/count/amount queries. Salesperson queries default to the main Kingdee salesperson and can include secondary/follow-up roles with `--all-staff`. If no template fits, `business sql` and `business query --fallback-sql` allow guarded read-only SQL against structured tables only; raw JSON and write statements are blocked. Detailed order rows stay in SQLite; the wiki remains for business context, customer/project knowledge, and later summary pages when you choose to generate them. Real Kingdee credentials live in `.runtime/kingdee.env`, and fetched tokens are cached in `.runtime/kingdee-token.json`; both files are local-only and ignored by git.
@@ -350,9 +359,9 @@ V1 uses fixed templates for monthly order count, monthly sales amount, monthly t
For daily unattended sync on Linux:
```bash
bash scripts/business_sync_service.sh start --vault /path/to/vault
bash scripts/business_sync_service.sh run-now --vault /path/to/vault
bash scripts/business_sync_service.sh status
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 runs as a user-level systemd timer, defaults to `07:10` daily, and uses `--incremental` with a 48-hour lookback window.
@@ -409,15 +418,15 @@ Client configuration example:
For long-running access from another machine, use the managed user service:
```bash
bash scripts/mcp_service.sh start
scripts/obsidian-wiki-ops.py mcp start
```
`mcp_service.sh start` creates `.runtime/obsidian-wiki-mcp.env` with `0600` permissions if it does not exist, writes `~/.config/systemd/user/obsidian-wiki-mcp.service`, starts the service, and prints the remote URL, `Authorization` header, enabled tool groups, config file path, and client JSON to copy into another agent. When `ingest` is enabled, it also prints the `/mcp-ingest/uploads` file upload endpoint. It serves the configured `OBSIDIAN_VAULT_PATH` by default; pass `--vault /path/to/vault` to override. The service defaults to `0.0.0.0:8765/mcp` because it is intended for remote clients, but it still requires the bearer token. Prefer SSH tunnel, Tailscale, WireGuard, or another trusted network boundary instead of directly exposing the port to the public internet.
`scripts/obsidian-wiki-ops.py mcp start` creates `.runtime/obsidian-wiki-mcp.env` with `0600` permissions if it does not exist, writes `~/.config/systemd/user/obsidian-wiki-mcp.service`, starts the service, and prints the remote URL, `Authorization` header, enabled tool groups, config file path, and client JSON to copy into another agent. When `ingest` is enabled, it also prints the `/mcp-ingest/uploads` file upload endpoint. It serves the configured `OBSIDIAN_VAULT_PATH` by default; pass `--vault /path/to/vault` to override. The service defaults to `0.0.0.0:8765/mcp` because it is intended for remote clients, but it still requires the bearer token. Prefer SSH tunnel, Tailscale, WireGuard, or another trusted network boundary instead of directly exposing the port to the public internet.
Customize the first startup if needed:
```bash
bash scripts/mcp_service.sh start --vault /path/to/vault --host 0.0.0.0 --port 9876 --path /wiki-mcp
scripts/obsidian-wiki-ops.py mcp start --vault /path/to/vault --host 0.0.0.0 --port 9876 --path /wiki-mcp
```
Tool groups are controlled by the same config file:
@@ -429,20 +438,20 @@ OBSIDIAN_MCP_ENABLED_TOOLS=file-assets
Edit that line directly, or use the service command:
```bash
bash scripts/mcp_service.sh tools list
bash scripts/mcp_service.sh tools enable ingest
bash scripts/mcp_service.sh tools disable ingest
bash scripts/mcp_service.sh tools enable query
bash scripts/mcp_service.sh tools disable query
scripts/obsidian-wiki-ops.py mcp tools list
scripts/obsidian-wiki-ops.py mcp tools enable ingest
scripts/obsidian-wiki-ops.py mcp tools disable ingest
scripts/obsidian-wiki-ops.py mcp tools enable query
scripts/obsidian-wiki-ops.py mcp tools disable query
```
To let a remote MCP client upload files and ask this machine's local OpenCode to run the normal `wiki-ingest` workflow, enable the `ingest` tool group and restart/start the same service:
```bash
bash scripts/mcp_service.sh start --enable-ingest
scripts/obsidian-wiki-ops.py mcp start --enable-ingest
```
After `ingest` is enabled once, later `bash scripts/mcp_service.sh start` preserves the config file setting. Use `bash scripts/mcp_service.sh start --disable-ingest` or `bash scripts/mcp_service.sh tools disable ingest` to turn it off.
After `ingest` is enabled once, later `scripts/obsidian-wiki-ops.py mcp start` preserves the config file setting. Use `scripts/obsidian-wiki-ops.py mcp start --disable-ingest` or `scripts/obsidian-wiki-ops.py mcp tools disable ingest` to turn it off.
This adds write-capable MCP tools to the same `wiki-file-assets` server and bearer token. The server is tool-first: some MCP clients may show empty `resources` or `prompts` lists until a concrete `asset://source/sha256/...` URI is returned by `get_file_asset_metadata`, so use the tool list as the source of truth.
@@ -474,10 +483,10 @@ Uploads land under `.runtime/mcp-ingest/uploads/`, outside the Obsidian vault, a
To let a remote MCP client ask synthesized questions against the compiled wiki, enable the `query` tool group:
```bash
bash scripts/mcp_service.sh start --enable-query
scripts/obsidian-wiki-ops.py mcp start --enable-query
```
After `query` is enabled once, later `bash scripts/mcp_service.sh start` preserves it. Use `bash scripts/mcp_service.sh start --disable-query` or `bash scripts/mcp_service.sh tools disable query` to turn it off.
After `query` is enabled once, later `scripts/obsidian-wiki-ops.py mcp start` preserves it. Use `scripts/obsidian-wiki-ops.py mcp start --disable-query` or `scripts/obsidian-wiki-ops.py mcp tools disable query` to turn it off.
This adds read-oriented OpenCode-backed query tools to the same `wiki-file-assets` server and bearer token:
@@ -492,19 +501,19 @@ The managed service config sets query to `OBSIDIAN_MCP_QUERY_OPENCODE_MODEL=open
Rotate the token later with:
```bash
bash scripts/mcp_service.sh new-token
scripts/obsidian-wiki-ops.py mcp new-token
```
Useful service commands:
```bash
bash scripts/mcp_service.sh show-config
bash scripts/mcp_service.sh tools list
bash scripts/mcp_service.sh tools enable ingest
bash scripts/mcp_service.sh tools disable ingest
bash scripts/mcp_service.sh stop
bash scripts/mcp_service.sh uninstall
bash scripts/mcp_service.sh status
scripts/obsidian-wiki-ops.py mcp show-config
scripts/obsidian-wiki-ops.py mcp tools list
scripts/obsidian-wiki-ops.py mcp tools enable ingest
scripts/obsidian-wiki-ops.py mcp tools disable ingest
scripts/obsidian-wiki-ops.py mcp stop
scripts/obsidian-wiki-ops.py mcp uninstall
scripts/obsidian-wiki-ops.py mcp status
```
### `_raw/` Staging Directory
+12 -12
View File
@@ -127,7 +127,7 @@ Knowledge that's project-specific goes under `projects/<name>/`. Knowledge that'
|---|---|---|
| `OBSIDIAN_SOURCES_DIR` | Directories with docs to ingest (comma-separated) | *(empty — point agent at specific files)* |
| `OBSIDIAN_BUSINESS_FACTS_DB` | Optional path for structured business facts such as Kingdee orders | `<vault>/_meta/business-facts.sqlite` |
| `.runtime/kingdee.env` | Private local Kingdee sales-order API config created by `obsidian-wiki business config kingdee` | official sales-order URL, blank credentials |
| `.runtime/kingdee.env` | Private local Kingdee sales-order API config created by `scripts/obsidian-wiki-ops.py kingdee config` | official sales-order URL, blank credentials |
| `OBSIDIAN_CATEGORIES` | Wiki page categories | `concepts,entities,skills,references,synthesis,journal` |
| `OBSIDIAN_MAX_PAGES_PER_INGEST` | Max pages updated per ingest | `15` |
| `CLAUDE_HISTORY_PATH` | Where to find Claude data | *auto-discovers from `~/.claude`* |
@@ -159,25 +159,25 @@ Knowledge that's project-specific goes under `projects/<name>/`. Knowledge that'
Use the business fact database for exact operational questions, for example monthly order counts or sales amounts. Do not ingest every order line into the wiki.
```bash
obsidian-wiki business sync kingdee --input ./kingdee-orders.json
scripts/obsidian-wiki-ops.py kingdee sync --input ./kingdee-orders.json
obsidian-wiki business config kingdee
scripts/obsidian-wiki-ops.py kingdee config
# Fill .runtime/kingdee.env, then test login/refresh without syncing orders:
obsidian-wiki business sync kingdee --dry-run-auth
scripts/obsidian-wiki-ops.py kingdee auth-test
obsidian-wiki business sync kingdee \
scripts/obsidian-wiki-ops.py kingdee sync \
--start-bizdate 2026-06-01 \
--end-bizdate 2026-06-30 \
--billstatus C
obsidian-wiki business sync kingdee --incremental
scripts/obsidian-wiki-ops.py kingdee sync --incremental
obsidian-wiki business query "2026年6月接了多少单"
obsidian-wiki business query "2026-06 sales amount"
obsidian-wiki business query "徐丽最近一周订单"
obsidian-wiki business query "徐丽最近一周销售额" --all-staff
obsidian-wiki business sql "SELECT COUNT(*) AS order_count FROM orders"
scripts/obsidian-wiki-ops.py kingdee query "2026年6月接了多少单"
scripts/obsidian-wiki-ops.py kingdee query "2026-06 sales amount"
scripts/obsidian-wiki-ops.py kingdee query "徐丽最近一周订单"
scripts/obsidian-wiki-ops.py kingdee query "徐丽最近一周销售额" --all-staff
scripts/obsidian-wiki-ops.py kingdee sql "SELECT COUNT(*) AS order_count FROM orders"
bash scripts/business_sync_service.sh start --vault /path/to/vault
scripts/obsidian-wiki-ops.py timer install --vault /path/to/vault
```
Without `--input`, the Kingdee sync command creates/reads `.runtime/kingdee.env`, obtains or refreshes an access token, calls the official sales order query API, then stores the fetched JSON response in `<vault>/_meta/business-facts.sqlite`. The private config supports configurable login/token URL, request body, response field paths, refresh URL/body, and required `x-acgw-identity`; fetched tokens are cached in `.runtime/kingdee-token.json`. `.runtime/` is ignored by git.
+18 -18
View File
@@ -17,27 +17,27 @@ The wiki remains the place for context: customer background, project history, sa
## Commands
```bash
obsidian-wiki business sync kingdee --input ./kingdee-orders.json
scripts/obsidian-wiki-ops.py kingdee sync --input ./kingdee-orders.json
obsidian-wiki business config kingdee
scripts/obsidian-wiki-ops.py kingdee config
# Fill .runtime/kingdee.env, then test the login/refresh flow:
obsidian-wiki business sync kingdee --dry-run-auth
scripts/obsidian-wiki-ops.py kingdee auth-test
obsidian-wiki business sync kingdee \
scripts/obsidian-wiki-ops.py kingdee sync \
--start-bizdate 2026-06-01 \
--end-bizdate 2026-06-30 \
--billstatus C
obsidian-wiki business sync kingdee --start-modifytime "2026-07-01 00:00:00"
obsidian-wiki business sync kingdee --incremental
obsidian-wiki business query "2026年6月接了多少单"
obsidian-wiki business query "2026-06 sales amount"
obsidian-wiki business query "2026-06 top customer"
obsidian-wiki business query "徐丽最近一周订单"
obsidian-wiki business query "徐丽最近一周接了多少单"
obsidian-wiki business query "徐丽最近一周销售额" --all-staff
obsidian-wiki business query "按组织列出订单" --fallback-sql "SELECT org_name, COUNT(*) AS order_count FROM orders GROUP BY org_name"
obsidian-wiki business sql "SELECT COUNT(*) AS order_count FROM orders"
scripts/obsidian-wiki-ops.py kingdee sync --start-modifytime "2026-07-01 00:00:00"
scripts/obsidian-wiki-ops.py kingdee sync --incremental
scripts/obsidian-wiki-ops.py kingdee query "2026年6月接了多少单"
scripts/obsidian-wiki-ops.py kingdee query "2026-06 sales amount"
scripts/obsidian-wiki-ops.py kingdee query "2026-06 top customer"
scripts/obsidian-wiki-ops.py kingdee query "徐丽最近一周订单"
scripts/obsidian-wiki-ops.py kingdee query "徐丽最近一周接了多少单"
scripts/obsidian-wiki-ops.py kingdee query "徐丽最近一周销售额" --all-staff
scripts/obsidian-wiki-ops.py kingdee query "按组织列出订单" --fallback-sql "SELECT org_name, COUNT(*) AS order_count FROM orders GROUP BY org_name"
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.
@@ -45,9 +45,9 @@ When `--input` is omitted, sync reads `.runtime/kingdee.env`, gets or refreshes
For unattended sync, install the user-level systemd timer:
```bash
bash scripts/business_sync_service.sh start --vault /path/to/vault
bash scripts/business_sync_service.sh run-now --vault /path/to/vault
bash scripts/business_sync_service.sh status
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.
@@ -115,7 +115,7 @@ If no fixed template matches, callers may provide guarded SQL through `business
## Incremental Sync
`obsidian-wiki business sync kingdee --incremental` uses Kingdee `start_modifytime`.
`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.
+1 -1
View File
@@ -2,4 +2,4 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 "$SCRIPT_DIR/manage_business_sync.py" "$@"
exec python3 "$SCRIPT_DIR/obsidian-wiki-ops.py" timer "$@"
+4 -1
View File
@@ -170,7 +170,10 @@ def start_timer(
) -> str:
paths = resolve_service_paths(project_root)
if not paths.config_path.exists():
raise RuntimeError(f"Missing Kingdee config file: {paths.config_path}. Run `obsidian-wiki business config kingdee` first.")
raise RuntimeError(
f"Missing Kingdee config file: {paths.config_path}. "
"Run `scripts/obsidian-wiki-ops.py kingdee config` first."
)
resolved_vault = resolve_vault_for_service(paths.project_root, vault_path=vault_path)
write_units(
paths,
+4 -1
View File
@@ -289,7 +289,10 @@ def write_token_env_file(
def read_service_env_file(env_path: Path) -> dict[str, str]:
if not env_path.exists():
raise RuntimeError(f"Missing MCP service env file: {env_path}. Run scripts/mcp_service.sh start to create it.")
raise RuntimeError(
f"Missing MCP service env file: {env_path}. "
"Run `scripts/obsidian-wiki-ops.py mcp start` to create it."
)
values: dict[str, str] = {}
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
+1 -1
View File
@@ -2,4 +2,4 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 "$SCRIPT_DIR/manage_mcp_service.py" "$@"
exec python3 "$SCRIPT_DIR/obsidian-wiki-ops.py" mcp "$@"
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Single user-facing operations entrypoint for this repository."""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
from typing import Sequence
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parent
for path in (REPO_ROOT, SCRIPT_DIR):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
from obsidian_wiki.cli import main as obsidian_cli_main
import manage_business_sync
import manage_mcp_service
HELP_TEXT = """obsidian-wiki user operations
Usage:
scripts/obsidian-wiki-ops.py help
scripts/obsidian-wiki-ops.py setup [args...]
scripts/obsidian-wiki-ops.py info
scripts/obsidian-wiki-ops.py file-index rebuild [args...]
scripts/obsidian-wiki-ops.py kingdee config [args...]
scripts/obsidian-wiki-ops.py kingdee auth-test [args...]
scripts/obsidian-wiki-ops.py kingdee sync [args...]
scripts/obsidian-wiki-ops.py kingdee query "2026年6月接了多少单" [args...]
scripts/obsidian-wiki-ops.py kingdee sql "SELECT COUNT(*) FROM orders" [args...]
scripts/obsidian-wiki-ops.py timer install [args...]
scripts/obsidian-wiki-ops.py timer run-now [args...]
scripts/obsidian-wiki-ops.py timer status
scripts/obsidian-wiki-ops.py timer stop
scripts/obsidian-wiki-ops.py timer uninstall [args...]
scripts/obsidian-wiki-ops.py mcp start [args...]
scripts/obsidian-wiki-ops.py mcp tools list
scripts/obsidian-wiki-ops.py mcp status
scripts/obsidian-wiki-ops.py mcp stop
scripts/obsidian-wiki-ops.py mcp uninstall
scripts/obsidian-wiki-ops.py daily-update [args...]
Notes:
timer install is an alias for installing and starting the Kingdee business sync timer.
Use --help after any forwarded command to see that command's detailed options.
"""
CLI_COMMANDS = {
"setup",
"list",
"info",
"file-index",
"business",
"mcp-server",
"bot-view",
}
def print_help() -> int:
print(HELP_TEXT, end="")
return 0
def run_script(script_name: str, args: Sequence[str]) -> int:
script = SCRIPT_DIR / script_name
completed = subprocess.run([str(script), *args], check=False)
return int(completed.returncode)
def route_kingdee(args: Sequence[str]) -> int:
if not args:
return print_help()
action, *rest = args
if action == "config":
return obsidian_cli_main(["business", "config", "kingdee", *rest])
if action == "auth-test":
return obsidian_cli_main(["business", "sync", "kingdee", "--dry-run-auth", *rest])
if action == "sync":
return obsidian_cli_main(["business", "sync", "kingdee", *rest])
if action == "query":
return obsidian_cli_main(["business", "query", *rest])
if action == "sql":
return obsidian_cli_main(["business", "sql", *rest])
print(f"Unknown kingdee action: {action}", file=sys.stderr)
return 2
def route_timer(args: Sequence[str]) -> int:
if not args:
return print_help()
action, *rest = args
action_aliases = {
"install": "start",
"start": "start",
"run": "run-now",
"now": "run-now",
"run-now": "run-now",
"status": "status",
"stop": "stop",
"uninstall": "uninstall",
}
mapped = action_aliases.get(action)
if mapped is None:
print(f"Unknown timer action: {action}", file=sys.stderr)
return 2
return manage_business_sync.main([mapped, *rest])
def main(argv: Sequence[str] | None = None) -> int:
args = list(sys.argv[1:] if argv is None else argv)
if not args or args[0] in {"help", "-h", "--help"}:
return print_help()
command, *rest = args
if command in CLI_COMMANDS:
return obsidian_cli_main([command, *rest])
if command == "kingdee":
return route_kingdee(rest)
if command == "timer":
return route_timer(rest)
if command == "mcp":
return manage_mcp_service.main(rest)
if command == "daily-update":
return run_script("daily-update.sh", rest)
print(f"Unknown command: {command}", file=sys.stderr)
print("Run `scripts/obsidian-wiki-ops.py help` for available operations.", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+8
View File
@@ -14,6 +14,14 @@ import manage_business_sync
class BusinessSyncServiceTests(unittest.TestCase):
def test_shell_entrypoint_delegates_to_unified_user_ops_entrypoint(self):
entrypoint = SCRIPTS_DIR / "business_sync_service.sh"
self.assertTrue(entrypoint.is_file())
self.assertTrue(os.access(entrypoint, os.X_OK))
content = entrypoint.read_text(encoding="utf-8")
self.assertIn('exec python3 "$SCRIPT_DIR/obsidian-wiki-ops.py" timer "$@"', content)
def test_render_service_and_timer_units_for_incremental_sync(self):
service = manage_business_sync.render_service_unit(
project_root=Path("/opt/obsidian-wiki"),
+3 -3
View File
@@ -15,7 +15,7 @@ import manage_mcp_service
class McpServiceConfigTests(unittest.TestCase):
def test_service_commands_use_one_shell_entrypoint(self):
def test_service_commands_delegate_to_unified_user_ops_entrypoint(self):
entrypoint = SCRIPTS_DIR / "mcp_service.sh"
legacy_wrappers = [
"new_mcp_service_token.sh",
@@ -28,9 +28,9 @@ class McpServiceConfigTests(unittest.TestCase):
self.assertTrue(entrypoint.is_file())
self.assertTrue(os.access(entrypoint, os.X_OK))
content = entrypoint.read_text(encoding="utf-8")
self.assertIn('exec python3 "$SCRIPT_DIR/manage_mcp_service.py" "$@"', content)
self.assertIn('exec python3 "$SCRIPT_DIR/obsidian-wiki-ops.py" mcp "$@"', content)
for wrapper in legacy_wrappers:
self.assertFalse((SCRIPTS_DIR / wrapper).exists(), f"{wrapper} should be folded into mcp_service.sh")
self.assertFalse((SCRIPTS_DIR / wrapper).exists(), f"{wrapper} should be folded into obsidian-wiki-ops.py")
def test_render_service_unit_uses_http_transport_without_embedding_token(self):
unit = manage_mcp_service.render_service_unit(
+68
View File
@@ -0,0 +1,68 @@
import importlib.util
import io
import sys
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest.mock import patch
SCRIPT_PATH = Path(__file__).resolve().parent.parent / "scripts" / "obsidian-wiki-ops.py"
def load_user_ops():
spec = importlib.util.spec_from_file_location("obsidian_wiki_user_ops", SCRIPT_PATH)
if spec is None or spec.loader is None:
raise ImportError(f"cannot import {SCRIPT_PATH}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class UserOpsTests(unittest.TestCase):
def test_kingdee_sync_forwards_to_business_cli(self):
user_ops = load_user_ops()
with patch.object(user_ops, "obsidian_cli_main", return_value=0) as cli_main:
status = user_ops.main(["kingdee", "sync", "--vault", "/data/vault", "--incremental"])
self.assertEqual(status, 0)
cli_main.assert_called_once_with(
["business", "sync", "kingdee", "--vault", "/data/vault", "--incremental"]
)
def test_timer_install_alias_installs_and_starts_business_timer(self):
user_ops = load_user_ops()
with patch.object(user_ops.manage_business_sync, "main", return_value=0) as timer_main:
status = user_ops.main(["timer", "install", "--vault", "/data/vault"])
self.assertEqual(status, 0)
timer_main.assert_called_once_with(["start", "--vault", "/data/vault"])
def test_mcp_commands_forward_to_mcp_service_manager(self):
user_ops = load_user_ops()
with patch.object(user_ops.manage_mcp_service, "main", return_value=0) as mcp_main:
status = user_ops.main(["mcp", "start", "--enable-query"])
self.assertEqual(status, 0)
mcp_main.assert_called_once_with(["start", "--enable-query"])
def test_help_lists_common_user_operations(self):
user_ops = load_user_ops()
output = io.StringIO()
with redirect_stdout(output):
status = user_ops.main(["help"])
self.assertEqual(status, 0)
text = output.getvalue()
self.assertIn("kingdee sync", text)
self.assertIn("timer install", text)
self.assertIn("mcp start", text)
if __name__ == "__main__":
unittest.main()