DeepSeek Harness Remote
DeepSeek Harness Remote 可將手機或另一台電腦與你運行 DeepSeek Harness 的電腦安全連接,隨時隨地繼續你的工作,方便隨時查看進度並繼續工作。
AI 翻譯
Astucia Wiki is a flat-file, AI-enabled, Git-enabled, self-hosted wiki that combines Markdown pages, draw.io diagrams, spreadsheet lists, and team chat — all stored as plain files on disk. No database.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗A flat-file, self-hosted wiki for teams. No database required — all content is stored as plain files on disk.
Astucia Wiki combines several content types in one place:
{include:ID} transclusion, and live preview/aiJob requests queued from the chat prompt for long, reasoning-heavy work that answers back in the thread when it finisheswiki_* ones, so an AI user can search vendor documentation, query a ticket tracker, or read another Astucia Wiki while answeringpageid links, folder hierarchy (parent/child/sibling pages), and shared tags; view the whole space or focus on one page's neighbourhood, with backlinks and related-page discoverymemory/ folder: one fact per page, listed back to them on every run, and readable, correctable and deletable by anyone who can read the spaceAuthentication is optional. When enabled it uses OIDC (tested with Auth0) with three roles: admin, editor, and reader.
git clone https://github.com/madsrg/astucia-wiki.git
cd astucia-wiki
composer install
cp config.php.txt config.php # edit config.php before starting
php -S localhost:8000
Open http://localhost:8000. No database, no build step.
One container (nginx + PHP-FPM + cron), with all pages, settings and logs on the host so
a backup is a tar of one directory and deleting the container loses nothing.
The image, the current tags and how to run it live on Docker Hub: madsrotwitt/astucia-wiki
DOCKER.md covers configuration, management, cron, backup/restore, and putting it behind nginx with Let's Encrypt.
config.php.txt is a template committed to the repository. Copy it to config.php and fill in your values before first use. Because config.php is excluded from version control, running git pull to update the wiki will never overwrite your live configuration.
The minimum required settings are PAGES_DIR (where wiki content is stored) and APP_TITLE. Everything else — authentication, email, logging — is optional and disabled by default.
Authentication is optional and off by default. AUTHENTICATION in config.php selects the method:
| Value | Behaviour |
|---|---|
'off' | No authentication — every visitor has full admin access (the default) |
'oidc' | OIDC / OAuth2 only (tested with Auth0, Keycloak, Okta) |
'otp' | One-time codes by email; users are added manually to users.json |
'both' | OIDC and OTP side by side; each user record carries an auth field |
For 'oidc' or 'both', also set the four OIDC constants (OIDC_PROVIDER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI). AUTHENTICATION_ENABLED still exists as a legacy shim, but it is derived from AUTHENTICATION — don't set it by hand.
Because you have server-side access during setup, the easiest way to create the first admin account is:
sub, name, and email into user_requests.json on the server.user_requests.json and find your entry. Copy the sub, name, and email into users.json (create it if it doesn't exist), add "role": "admin" and "spaces": null:{
"users": [
{
"sub": "<paste sub from user_requests.json>",
"name": "Your Name",
"email": "[email protected]",
"role": "admin",
"spaces": null
}
]
}
No need to look up your sub ID in the provider dashboard; the wiki captures it automatically on first login. After bootstrap, users.json is managed entirely through the Admin panel.
The admin panel (gear icon, Users tab) lets you create two kinds of non-human users.
An AI user is a bot that participates in chat threads. When a regular user #mentions it, the wiki sends the conversation to a language model and posts the reply automatically. The bot's role controls page-writing access: an editor AI user can create and overwrite pages; a reader AI user can reply in chat and read pages but cannot write.
Configuration per AI user:
| Field | Description |
|---|---|
| Name | The #mention handle that triggers the bot |
| Provider | One of the entries in llm_providers.json — openai (Chat Completions, default), openai-responses (OpenAI Responses API), or anthropic |
| API URL | Leave blank for the provider's default endpoint, or set a custom endpoint for self-hosted / compatible models |
| Model | e.g. gpt-4o, claude-opus-4-8, or any model string accepted by the endpoint |
| API key | Stored server-side only — never sent to the browser |
| System prompt | Optional instructions that set the bot's persona or constraints |
| Context messages | How many recent chat messages to include in each request (default 20) |
| Max tokens | Maximum length of the model's reply (default 4096) |
| Temperature | Sampling temperature, 0–2 (default 0.7) |
The available providers come from llm_providers.json in the project root. Each entry maps a provider id to a response family (openai-chat, openai-responses, or anthropic — the three wire formats the wiki knows how to build and parse), a display label, and a default endpoint. To add a provider variant that speaks one of those families (a different endpoint, a compatible service), add an entry to that file — no code change. The wiki also self-corrects common OpenAI drift automatically: it picks max_tokens vs max_completion_tokens per endpoint and retries if the API asks for the other, and drops an unsupported custom temperature for reasoning models.
AI users also receive a service token (wk_ai_…) that can be used to call the API on their behalf from external scripts.
AI users (and MCP clients — see below) can use the tools the wiki exposes:
wiki_list_pages — lists all pages in the current Space, including their tagswiki_search_pages — searches pages by topic, recency, and/or tagswiki_read_page — reads the content of any .md, .list, or .chat filewiki_write_page — creates or overwrites a .md page (editor role only)wiki_related_pages — finds pages related to a given page via the knowledge graph (links, folder hierarchy, and shared tags)wiki_add_tags — adds tags to a page without removing existing ones (editor role only)wiki_set_tags — sets or clears the tags on any page (editor role only)A system user is a headless account for external integrations (scripts, CI pipelines, other services). It has no AI config — it authenticates via a service token (wk_sys_…) sent as Authorization: Bearer <token> and can call any API action its role permits.
Create a system user in the admin panel, copy the generated token, and use it in your integration. Tokens can be regenerated at any time.
The wiki also speaks the Model Context Protocol. Point any MCP client (Claude Desktop, IDE agents, custom tooling) at mcp.php and it can call the same wiki_* tools listed above over JSON-RPC 2.0 (Streamable HTTP transport). Authentication uses the same service tokens as the REST API — an AI user's wk_ai_… token or a system user's wk_sys_… token as Authorization: Bearer <token>. Add ?space=SpaceName to target a specific Space (omit for the default). Role and per-Space access control apply exactly as they do everywhere else. An in-app MCP Tool Explorer (admin/editor sidebar) lets you try the tools interactively.
The wiki is also an MCP client, so the tools an AI user can reach are not limited to the
wiki's own. Register a server once in Admin → MCP Servers (stored in
WIKI_SYSTEM_DATA/mcp_servers.json) and tick it on each AI user that should be allowed to
call it; its tools then appear beside the wiki_* tools in the same agent loop. This works
on all three AI paths — an inline chat reply, a one-off /aiJob, and a scheduled agent job.
initialize, any
Mcp-Session-Id the server assigns, notifications/initialized, then tools/list and
tools/call). Stateless servers that return no session id work unchanged.Authorization: Bearer <token> by
default. Both parts are configurable, so a server wanting X-API-Key: <raw token> is a
matter of setting the header name and clearing the scheme. Arbitrary extra headers can
be sent alongside it, for a gateway such as a Cloudflare Access tunnel that needs
CF-Access-Client-Id / CF-Access-Client-Secret. Test Connection on the server form
lists the tools it advertises.servername__toolname, so a remote tool can never
collide with a built-in wiki_* tool or with another server's tool of the same name —
which matters immediately if you point one Astucia Wiki at another's mcp.php.list_projects before
get_project") that are sent to every AI user that enables it; an AI user can add a note
covering only what is true of that AI ("you are read-only here"). Both are sent when both
are filled in, server first — so ten AI users sharing a server need one copy of the text,
edited in one place.src:<slug> forces a server. Putting src:jira in a chat message or an agent-job
prompt restricts that reply to that server's tools — a deterministic alternative to the
free-text instructions, which only hint..search page can run against a remote
server instead of the local index; the search tool and its query argument are auto-detected
(a tool named like search/find/query, an argument named query) or named explicitly.
Marking a server as "This server is an Astucia Wiki" additionally enables tag: and
updated: filters and renders native page results, which is federated search across two
wikis.An agent job runs an AI user against a prompt in the background, with the wiki tools available, so it can read and write pages on its own. There are two kinds:
/aiJob #AiUser your request. Use these for a request that is too slow or too involved for an inline chat reply: the AI user is asked to reason at length, so the job may take many minutes. Submitting only confirms acceptance ("job accepted — starts in about N minutes"); the answer arrives as a normal chat message in the same thread when it is done, and optionally by email (My Preferences → notify me when my AI jobs finish). Readers cannot queue jobs, and each person may have three waiting at a time.Both kinds are executed by one CLI script run from cron. Without this entry, scheduled jobs never fire and one-off jobs are accepted but never start (the chat prompt will say so, and Admin → AI shows a warning):
sudo -u www-data crontab -e
*/2 * * * * php /path/to/run_ai_agent_jobs.php >> /var/log/wiki-agent-jobs.log 2>&1
Keep AGENT_JOB_RUNNER_INTERVAL_MINUTES in config.php equal to the cron interval — it is what the "starts in about N minutes" estimate is calculated from, so a mismatch produces a wrong estimate. Each run executes at most three one-off jobs; the rest wait for the next tick. AGENT_JOB_RUNNER_SLOTS (default 2) is how many runs may overlap: with one slot the queue is serial, so a slow job delays every job behind it, and each extra slot is another simultaneous LLM call. Per-run logs are written to LOG_DIR/agent-jobs/, viewable in the admin panel, and failures are emailed to ADMIN_EMAIL.
Which model an AI user runs matters here: reasoning-capable models (Claude Opus/Sonnet 4.6 and newer, OpenAI o-series and GPT-5) are asked to think at length for one-off jobs, while older models simply run the prompt normally. Per-model request rules live in llm_providers.json under model_rules and can be adjusted without code changes.
An AI user can keep what it learns. Switch it on in two places — Space settings → AI decides whether that space keeps memories at all, and Admin → AI Users → Learning decides whether a given AI keeps any (with a wiki-wide default for "use the house setting"). Nothing else to configure: no separate memory store, no vector database, no service token.
A memory is a page. They live in a memory/ folder inside the space, one fact per page, created the first time something is remembered:
Team/
memory/
Deploys go out on Thursday afternoons.md
Rita owns the billing integration.md
Each one carries who formed it and when, and is tagged rather than filed into subfolders — a wrong tag still turns up in search, while a wrong folder is a fact that is never found again. The AI sees the list of titles in every run, so it knows what it knows, and reads the full text of one only when it matters.
Because memories are pages, you can open the folder and read exactly what an AI believes, correct it, or delete it; the changes are in git like any other edit. They are deliberately kept out of ordinary search results, the knowledge graph and mention notifications, so a few hundred remembered facts do not drown everything else.
Two properties worth knowing:
Turned on with ENABLE_REALTIME, the wiki pushes changes over a single SSE connection instead of leaving the UI to poll: a chat message, a page edit or a finished agent job reaches an open browser in a fraction of a second rather than after the next tick. Every poller keeps its timer as a slower safety net, so ENABLE_REALTIME=false behaves exactly like every release before the feature existed.
Push is served by a Mercure hub. The Docker image ships one and starts it automatically. On a bare install, tools/install-mercure.sh does the whole job — it downloads the release and verifies its checksum, creates a locked-down service account, generates the shared key, writes the hub config and a hardened systemd unit, starts it and checks that it answers:
sudo ./tools/install-mercure.sh 1.1.5
It shows what it is about to do and waits for a key press (MERCURE_ASSUME_YES=1 skips that for unattended runs), prints every path it writes, and leaves the config.php constants (key included) in /etc/mercure/wiki-config-snippet.php along with the nginx block you still need. Re-run it with a newer version to upgrade in place; the key is never regenerated, since config.php holds a copy.
One hub serves one wiki. Every wiki publishes the same topic names, so two wikis sharing a hub would receive each other's change events, and with one key either could sign tickets the other accepts. To run several wikis on one machine, give each its own instance:
sudo MERCURE_INSTANCE=team ./tools/install-mercure.sh 1.1.5
A named instance gets its own config directory (/etc/mercure-team/), service (mercure-team), service account, key, port (the first free one from 3001), and issuer, audience and cookie name — so copy all of its snippet into that wiki's config.php, not only the key. Each wiki also needs its own hostname, since the hub is always reached at /.well-known/mercure. Without MERCURE_INSTANCE the script works on the default instance, exactly as a single-wiki install always has. Upgrading upgrades every instance: they share one binary, so the script finds them all, regenerates each one's configuration from the settings it was installed with, and restarts the ones that were running.
The hub must be Mercure 1.0.0 or newer, and you want 1.1.5. 1.0 replaced the protocol's bespoke JWT claim with an ordinary OAuth 2.0 access token, and the wiki signs those — a 0.x hub answers 401 to every publish. The installer refuses a 0.x version rather than leaving you with a hub that looks healthy and delivers nothing, and warns if you ask for anything older than 1.1.0, the newest release fixing something reachable in this setup. (1.0.1 was tagged but never built, and 1.1.3 tagged but never released.) Upgrading an existing install means re-running the script and adding the three constants it now writes into the snippet (MERCURE_ISSUER, MERCURE_RESOURCE_ID, MERCURE_COOKIE_NAME); the hub and the wiki have to agree on all three. Docker installs need nothing: the image ships the hub and configures both sides. Because an SSE stream occupies one of the browser's ~6 connections per origin, realtime wants HTTP/2 — that is, TLS at your reverse proxy. It works over HTTP/1.1, but a few open tabs will exhaust the connection budget.
Events are hints, not payloads. An event says "this thread changed, re-read it"; it never carries the message. Reading still goes through the API and its guards, so nothing bypasses Space or role permissions, and a subscriber that missed events while disconnected is correct again as soon as it re-reads.
An integration subscribes with the same wk_sys_… service token it already uses for the REST API. Ask for a ticket, then open the stream with it:
# 1. Mint a subscriber ticket (scoped to the Spaces this token may read)
TICKET=$(curl -s -H "Authorization: Bearer $WIKI_TOKEN" \
"https://wiki.example.com/api.php?action=realtime_ticket" | jq -r .token)
# 2. Stream events. 'wiki/*' is a URL Pattern meaning "everything the ticket allows".
curl -N -H "Authorization: Bearer $TICKET" \
--data-urlencode 'match_urlpattern=wiki/*' -G \
"https://wiki.example.com/.well-known/mercure"
The stream then stays open, one event per change (the leading : is the hub's keep-alive):
:
id: urn:uuid:01a082bc-a23e-7e5c-a9af-02c3f1c78237
data: {"type":"page","space":"Main","path":"Notes.md","change":"create","topic":"wiki/Main/page/Notes.md","ts":1788899795}
id: urn:uuid:01a082bc-a246-7e5c-a305-fbef5f1b8a89
data: {"type":"tree","space":"Main","topic":"wiki/Main/tree","ts":1788899795}
A runnable version with reconnection and ticket renewal is in examples/watch_wiki.py — standard library only, no SSE client needed:
WIKI_URL=https://wiki.example.com WIKI_TOKEN=wk_sys_... ./examples/watch_wiki.py
There are five topics, and that set is the contract:
| Topic | Published when |
|---|---|
wiki/{space}/chat/{path} | a message is posted, edited or deleted, or an AI placeholder resolves |
wiki/{space}/page/{path} | a page is saved, created, renamed or deleted |
wiki/{space}/tree | the file tree changes, including from an external edit to PAGES_DIR |
wiki/user/{uid}/job | one of that user's background jobs changes state |
wiki/user/{uid}/mention | that user is mentioned |
Notes for integrators:
wiki/* cannot widen it.authorization query parameter outright, so a token in the URL is no longer accepted anywhere.REALTIME_TICKET_TTL, one hour by default). Mint a fresh one when the stream drops; that is also what makes revoking a Space take effect.url.Write a sequence diagram, flowchart, state chart, ER diagram or gantt chart as a fenced code block in any Markdown page and it renders as a diagram when you read the page:
```mermaid
sequenceDiagram
autonumber
Alice->>Bob: Request
Bob-->>Alice: Response
```
The Insert menu in the editor has starter blocks for sequence diagrams and flowcharts. Because the diagram is just text in the page, it lives in that page's version history, it shows up in search by its own labels, and editing it is editing the page — no separate editor and no separate file to keep in sync. A page containing only a diagram can be embedded in any other page with {include:ID}, which is the way to reuse one diagram in several places. Diagrams also render in exported static sites.
If a diagram has a syntax error, the page shows the error together with the source that caused it rather than dropping the block, so it can be fixed in place. Draw.io diagrams (.drawio files) remain available for anything better drawn by hand.
Every Space can be viewed as a knowledge graph — an interactive map of how its pages relate. It layers three kinds of relationship:
?pageid= references between pages (directed edges)Open the whole-space map from the sidebar (graph icon), or focus on the current page's neighbourhood from the page header. Nodes are coloured by top-level folder and sized by how connected they are; each edge type can be toggled on or off, and clicking a page navigates to it. The relationships also power backlinks, related-page discovery, and the wiki_related_pages tool for AI/MCP clients.
The graph is derived from the page index and cached per Space (graph.json); it refreshes incrementally as pages change and rebuilds fully on a manual reindex — no separate maintenance step is required.
If the content directory (or PAGES_DIR itself) is a git repository, Astucia Wiki will automatically commit changes to it:
The clock icon in the header opens the full commit history for the current file.
To initialise git tracking for your content, run as the web-server user (typically www-data) so that subsequent auto-commits have the right ownership:
sudo -u www-data git -C /path/to/your/PAGES_DIR init
sudo -u www-data git -C /path/to/your/PAGES_DIR add .
sudo -u www-data git -C /path/to/your/PAGES_DIR commit -m "Initial content"
Because content is plain files, you can work on it however you like — rsync a folder in, git pull on the content repository, run a script, edit with a desktop editor over a network share. Astucia Wiki notices: added, removed and externally edited pages are reconciled with the page index, the knowledge-graph cache and the search index automatically, and the file tree refreshes on its own.
The page you are looking at reloads itself when its file changes underneath — Markdown pages, lists and data pages, within about ten seconds. If you are editing, nothing is touched: you get a notice that the page changed on disk instead, because saving would otherwise overwrite that change without warning.
There is no cron job and no filesystem watcher to install — the check runs as part of ordinary requests, is skipped unless INDEX_SYNC_INTERVAL_SECONDS (default 30) has elapsed since the last look, and is shared safely between simultaneous visitors. It also runs on the MCP endpoint, so an AI client reconciles before it reads rather than answering from a stale index. Existing pages keep their IDs, so ?pageid= links and bookmarks survive.
Lower INDEX_SYNC_INTERVAL_SECONDS to react sooner, raise it to do less work on a very large content directory. ?action=indexfiles still forces a full rebuild on demand.
The UI is available in English, Danish, Swedish, Spanish, French, German, Simplified Chinese, and Hindi. Logged-in users pick their language from My Preferences; anonymous / no-auth visitors get a language selector in the sidebar instead. The choice is stored in the browser. Adding a new language requires one new file in modules/i18n/locales/ — copy en.js, translate the strings, and add the language code to SUPPORTED_LANGUAGES in modules/i18n/index.js.
Translations don't have to be complete: any key a locale is missing falls back to English, so the admin panel is intentionally English-only.
Users can opt in to a daily digest email from My Preferences → "Subscribe to daily updates" (shown when email is configured). Once a day it summarises pages created or updated in the last 24 hours across every space the user can access, excluding their own edits, capped at the 20 most recent changes and grouped by space.
Delivery is driven by a CLI script run from cron — the same pattern as AI Agent Jobs. Edit the web-server user's crontab (www-data on Debian):
sudo -u www-data crontab -e
and add one entry to run the digest once a day, e.g. 07:00:
0 7 * * * php /path/to/run_daily_digest.php >> /var/log/wiki-digest.log 2>&1
A user's crontab isn't a file in their home directory — it lives in the cron spool (on Debian, /var/spool/cron/crontabs/www-data); always edit it via crontab -e rather than by hand. Running as www-data keeps file and email permissions consistent with the rest of the wiki.
Email links use APP_BASE_URL from config.php (falling back to the origin of OIDC_REDIRECT_URI), since a cron run has no web request to derive the host from.
Full documentation, feature guides, and a step-by-step installation guide for Debian with Nginx and PHP are available at:
jumbojett/openid-connect-php (optional)export_static_site.php — generates a self-contained HTML site from all pagesCopyright (C) 2026 Mads Rotwitt
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
The full licence text is in LICENSE (GPL-3.0-or-later).
In short: you may use, study, modify and redistribute it, including commercially. The obligations attach to distribution — if you give others a copy of a modified version, they must be able to get the corresponding source under the same licence, with copyright and licence notices intact. Hosting it as a service, where users never receive a copy, is not distribution under GPL v3 (that would be the AGPL). See the licence page for a plain-language summary.