@biaoo/tiangong-wiki 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -1
- package/README.zh-CN.md +106 -1
- package/dist/commands/create.js +3 -0
- package/dist/commands/sync.js +3 -0
- package/dist/commands/template.js +3 -0
- package/dist/core/db.js +32 -1
- package/dist/core/page-source.js +25 -0
- package/dist/core/paths.js +1 -0
- package/dist/core/runtime.js +2 -2
- package/dist/core/sync.js +6 -5
- package/dist/daemon/audit-log.js +18 -0
- package/dist/daemon/client.js +1 -1
- package/dist/daemon/git-journal.js +114 -0
- package/dist/daemon/server.js +446 -124
- package/dist/daemon/write-actor.js +60 -0
- package/dist/daemon/write-queue.js +360 -0
- package/dist/operations/dashboard.js +4 -9
- package/dist/operations/query.js +7 -1
- package/dist/operations/write.js +93 -5
- package/mcp-server/dist/daemon-client.js +90 -0
- package/mcp-server/dist/index.js +26 -0
- package/mcp-server/dist/server.js +525 -0
- package/package.json +11 -5
- package/references/centralized-service-deployment.md +482 -0
- package/references/examples/centralized-service/centralized.env.example +25 -0
- package/references/examples/centralized-service/nginx-centralized-wiki.conf +68 -0
- package/references/examples/centralized-service/tiangong-wiki-daemon.service +17 -0
- package/references/examples/centralized-service/tiangong-wiki-mcp.service +18 -0
- package/references/troubleshooting.md +15 -0
package/README.md
CHANGED
|
@@ -99,7 +99,112 @@ tiangong-wiki dashboard # open dashboard in browse
|
|
|
99
99
|
# or: tiangong-wiki daemon run # run the daemon in the foreground for debugging
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
> Environment variables are managed via `.wiki.env` (created by `tiangong-wiki setup`). The CLI prefers the nearest local `.wiki.env`, then falls back to the global default workspace config. See [references/troubleshooting.md](./references/troubleshooting.md) for the full reference.
|
|
102
|
+
> Environment variables are managed via `.wiki.env` (created by `tiangong-wiki setup`). The CLI prefers the nearest local `.wiki.env`, then falls back to the global default workspace config. See [references/troubleshooting.md](./references/troubleshooting.md) for the full reference. For a centralized Linux + `systemd` + Nginx deployment, see [references/centralized-service-deployment.md](./references/centralized-service-deployment.md). That deployment guide now also includes Git repository / GitHub remote setup for daemon-side commit and optional auto-push.
|
|
103
|
+
|
|
104
|
+
## MCP Server
|
|
105
|
+
|
|
106
|
+
Tiangong Wiki ships a separate MCP adapter that talks to the daemon over HTTP. It uses the MCP Streamable HTTP transport, not stdio.
|
|
107
|
+
|
|
108
|
+
Start it after the daemon is already listening:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
tiangong-wiki daemon run
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
In another shell:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
export WIKI_DAEMON_BASE_URL=http://127.0.0.1:8787
|
|
118
|
+
export WIKI_MCP_HOST=127.0.0.1
|
|
119
|
+
export WIKI_MCP_PORT=9400
|
|
120
|
+
export WIKI_MCP_PATH=/mcp
|
|
121
|
+
|
|
122
|
+
tiangong-wiki-mcp-server
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The server prints a JSON line like:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{"status":"listening","host":"127.0.0.1","port":9400,"healthUrl":"http://127.0.0.1:9400/health","mcpUrl":"http://127.0.0.1:9400/mcp"}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
If you are running from a source checkout instead of a global install:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm install
|
|
135
|
+
npm run build
|
|
136
|
+
|
|
137
|
+
WIKI_DAEMON_BASE_URL=http://127.0.0.1:8787 \
|
|
138
|
+
WIKI_MCP_PORT=9400 \
|
|
139
|
+
node mcp-server/dist/index.js
|
|
140
|
+
|
|
141
|
+
# or during development
|
|
142
|
+
npm run dev:mcp-server
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Required MCP-side environment variables:
|
|
146
|
+
|
|
147
|
+
- `WIKI_DAEMON_BASE_URL`: base URL of the wiki daemon, for example `http://127.0.0.1:8787`
|
|
148
|
+
- `WIKI_MCP_HOST`: bind host for the MCP HTTP server, default `127.0.0.1`
|
|
149
|
+
- `WIKI_MCP_PORT`: bind port for the MCP HTTP server, default random free port
|
|
150
|
+
- `WIKI_MCP_PATH`: MCP route path, default `/mcp`
|
|
151
|
+
|
|
152
|
+
Bearer token note:
|
|
153
|
+
|
|
154
|
+
- Bearer tokens are not configured in `.wiki.env`, daemon env, or MCP env
|
|
155
|
+
- In the current V1 deployment model, Bearer tokens live in the reverse proxy config
|
|
156
|
+
- See [references/examples/centralized-service/nginx-centralized-wiki.conf](./references/examples/centralized-service/nginx-centralized-wiki.conf) for the current `map $http_authorization ...` example
|
|
157
|
+
- In production, keep token values in a private Nginx include file such as `/etc/nginx/snippets/wiki-auth-tokens.conf`, then `include` it from the main site config instead of hardcoding secrets in the repo
|
|
158
|
+
|
|
159
|
+
## Using the MCP From Clients
|
|
160
|
+
|
|
161
|
+
Any MCP client that supports Streamable HTTP can connect to the MCP endpoint:
|
|
162
|
+
|
|
163
|
+
- Local debug endpoint: `http://127.0.0.1:9400/mcp`
|
|
164
|
+
- Health check: `http://127.0.0.1:9400/health`
|
|
165
|
+
- Production recommendation: expose `/mcp` behind a reverse proxy and keep daemon/MCP bound to loopback
|
|
166
|
+
|
|
167
|
+
Read tools can be called directly. Write tools such as `wiki_page_create`, `wiki_page_update`, and `wiki_sync` require these headers:
|
|
168
|
+
|
|
169
|
+
- `x-wiki-actor-id`
|
|
170
|
+
- `x-wiki-actor-type`
|
|
171
|
+
- `x-request-id`
|
|
172
|
+
|
|
173
|
+
In production, the recommended model is: the client sends `Authorization: Bearer ...` to the reverse proxy, the proxy validates the token there, and the proxy injects the actor headers before forwarding to the MCP server. For local debugging without a proxy, your client must send those headers itself when calling write tools.
|
|
174
|
+
|
|
175
|
+
Minimal Node.js MCP client example:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
179
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
180
|
+
|
|
181
|
+
const transport = new StreamableHTTPClientTransport(new URL("http://127.0.0.1:9400/mcp"), {
|
|
182
|
+
requestInit: {
|
|
183
|
+
headers: {
|
|
184
|
+
"x-wiki-actor-id": "agent:demo",
|
|
185
|
+
"x-wiki-actor-type": "agent",
|
|
186
|
+
"x-request-id": "req-demo-1",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const client = new Client({ name: "demo-client", version: "1.0.0" });
|
|
192
|
+
await client.connect(transport);
|
|
193
|
+
|
|
194
|
+
const tools = await client.listTools();
|
|
195
|
+
const search = await client.callTool({
|
|
196
|
+
name: "wiki_search",
|
|
197
|
+
arguments: { query: "bayes", limit: 5 },
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Current MCP tools include:
|
|
202
|
+
|
|
203
|
+
- Query: `wiki_find`, `wiki_fts`, `wiki_search`, `wiki_graph`
|
|
204
|
+
- Page: `wiki_page_info`, `wiki_page_read`, `wiki_page_create`, `wiki_page_update`
|
|
205
|
+
- Type: `wiki_type_list`, `wiki_type_show`, `wiki_type_recommend`
|
|
206
|
+
- Vault: `wiki_vault_list`, `wiki_vault_queue`
|
|
207
|
+
- Maintenance: `wiki_sync`, `wiki_lint`
|
|
103
208
|
|
|
104
209
|
## CLI
|
|
105
210
|
|
package/README.zh-CN.md
CHANGED
|
@@ -99,7 +99,112 @@ tiangong-wiki dashboard # 在浏览器中打开仪
|
|
|
99
99
|
# 或者:tiangong-wiki daemon run # 前台运行 daemon,适合调试
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
> 环境变量通过 `.wiki.env` 管理(由 `tiangong-wiki setup` 创建)。CLI 会优先使用最近的本地 `.wiki.env`,找不到时再 fallback 到全局默认工作区配置。完整参考见 [references/troubleshooting.md](./references/troubleshooting.md)
|
|
102
|
+
> 环境变量通过 `.wiki.env` 管理(由 `tiangong-wiki setup` 创建)。CLI 会优先使用最近的本地 `.wiki.env`,找不到时再 fallback 到全局默认工作区配置。完整参考见 [references/troubleshooting.md](./references/troubleshooting.md)。如需部署中心化服务(Linux + `systemd` + Nginx),见 [references/centralized-service-deployment.md](./references/centralized-service-deployment.md)。该部署文档现在也包含了 Git 仓库初始化、GitHub remote 配置和 daemon 自动 push 的 Git 配置说明。
|
|
103
|
+
|
|
104
|
+
## MCP Server
|
|
105
|
+
|
|
106
|
+
Tiangong Wiki 提供了独立的 MCP 适配层,通过 HTTP 调用 daemon。它使用的是 MCP 的 Streamable HTTP 传输,不是 stdio。
|
|
107
|
+
|
|
108
|
+
启动 MCP 前,先确保 daemon 已经在监听:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
tiangong-wiki daemon run
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
另开一个终端:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
export WIKI_DAEMON_BASE_URL=http://127.0.0.1:8787
|
|
118
|
+
export WIKI_MCP_HOST=127.0.0.1
|
|
119
|
+
export WIKI_MCP_PORT=9400
|
|
120
|
+
export WIKI_MCP_PATH=/mcp
|
|
121
|
+
|
|
122
|
+
tiangong-wiki-mcp-server
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
启动后会输出一行 JSON,例如:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{"status":"listening","host":"127.0.0.1","port":9400,"healthUrl":"http://127.0.0.1:9400/health","mcpUrl":"http://127.0.0.1:9400/mcp"}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
如果你不是通过全局安装使用,而是在源码仓库里运行:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm install
|
|
135
|
+
npm run build
|
|
136
|
+
|
|
137
|
+
WIKI_DAEMON_BASE_URL=http://127.0.0.1:8787 \
|
|
138
|
+
WIKI_MCP_PORT=9400 \
|
|
139
|
+
node mcp-server/dist/index.js
|
|
140
|
+
|
|
141
|
+
# 或者开发态运行
|
|
142
|
+
npm run dev:mcp-server
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
MCP 侧需要的环境变量:
|
|
146
|
+
|
|
147
|
+
- `WIKI_DAEMON_BASE_URL`:wiki daemon 的 base URL,例如 `http://127.0.0.1:8787`
|
|
148
|
+
- `WIKI_MCP_HOST`:MCP HTTP 服务绑定地址,默认 `127.0.0.1`
|
|
149
|
+
- `WIKI_MCP_PORT`:MCP HTTP 服务绑定端口,默认随机空闲端口
|
|
150
|
+
- `WIKI_MCP_PATH`:MCP 路由路径,默认 `/mcp`
|
|
151
|
+
|
|
152
|
+
Bearer token 说明:
|
|
153
|
+
|
|
154
|
+
- Bearer token 不配置在 `.wiki.env`、daemon env 或 MCP env 里
|
|
155
|
+
- 当前 V1 部署模型里,Bearer token 配在反向代理层
|
|
156
|
+
- 具体示例见 [references/examples/centralized-service/nginx-centralized-wiki.conf](./references/examples/centralized-service/nginx-centralized-wiki.conf) 中的 `map $http_authorization ...`
|
|
157
|
+
- 生产环境建议把 token 放在私有的 Nginx include 文件中,例如 `/etc/nginx/snippets/wiki-auth-tokens.conf`,再由主站点配置 `include` 进来,不要把真实密钥硬编码在仓库示例里
|
|
158
|
+
|
|
159
|
+
## 客户端如何使用这个 MCP
|
|
160
|
+
|
|
161
|
+
任何支持 Streamable HTTP 的 MCP client 都可以连接到这个服务:
|
|
162
|
+
|
|
163
|
+
- 本地调试地址:`http://127.0.0.1:9400/mcp`
|
|
164
|
+
- 健康检查:`http://127.0.0.1:9400/health`
|
|
165
|
+
- 生产环境建议:对外只暴露反向代理后的 `/mcp`,daemon 和 MCP 自身只监听 loopback
|
|
166
|
+
|
|
167
|
+
读工具可以直接调用。写工具,如 `wiki_page_create`、`wiki_page_update`、`wiki_sync`,额外要求这些 header:
|
|
168
|
+
|
|
169
|
+
- `x-wiki-actor-id`
|
|
170
|
+
- `x-wiki-actor-type`
|
|
171
|
+
- `x-request-id`
|
|
172
|
+
|
|
173
|
+
生产环境推荐模式是:客户端只向反向代理发送 `Authorization: Bearer ...`,由反向代理在代理层完成 token 校验,并在转发到 MCP server 前注入 actor headers。若你在本地直接连 MCP 做调试,则需要由客户端自己带上这些 header 才能调用写工具。
|
|
174
|
+
|
|
175
|
+
最小 Node.js MCP client 示例:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
179
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
180
|
+
|
|
181
|
+
const transport = new StreamableHTTPClientTransport(new URL("http://127.0.0.1:9400/mcp"), {
|
|
182
|
+
requestInit: {
|
|
183
|
+
headers: {
|
|
184
|
+
"x-wiki-actor-id": "agent:demo",
|
|
185
|
+
"x-wiki-actor-type": "agent",
|
|
186
|
+
"x-request-id": "req-demo-1",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const client = new Client({ name: "demo-client", version: "1.0.0" });
|
|
192
|
+
await client.connect(transport);
|
|
193
|
+
|
|
194
|
+
const tools = await client.listTools();
|
|
195
|
+
const search = await client.callTool({
|
|
196
|
+
name: "wiki_search",
|
|
197
|
+
arguments: { query: "bayes", limit: 5 },
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
当前 MCP tools 包括:
|
|
202
|
+
|
|
203
|
+
- 查询:`wiki_find`、`wiki_fts`、`wiki_search`、`wiki_graph`
|
|
204
|
+
- 页面:`wiki_page_info`、`wiki_page_read`、`wiki_page_create`、`wiki_page_update`
|
|
205
|
+
- 类型:`wiki_type_list`、`wiki_type_show`、`wiki_type_recommend`
|
|
206
|
+
- Vault:`wiki_vault_list`、`wiki_vault_queue`
|
|
207
|
+
- 维护:`wiki_sync`、`wiki_lint`
|
|
103
208
|
|
|
104
209
|
## CLI
|
|
105
210
|
|
package/dist/commands/create.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
|
|
2
|
+
import { buildCliWriteActor } from "../daemon/write-actor.js";
|
|
2
3
|
import { createPage } from "../operations/write.js";
|
|
3
4
|
import { writeJson } from "../utils/output.js";
|
|
4
5
|
export function registerCreateCommand(program) {
|
|
@@ -20,7 +21,9 @@ export function registerCreateCommand(program) {
|
|
|
20
21
|
endpoint,
|
|
21
22
|
method: "POST",
|
|
22
23
|
path: "/create",
|
|
24
|
+
timeoutMs: 310_000,
|
|
23
25
|
body: {
|
|
26
|
+
actor: buildCliWriteActor(process.env),
|
|
24
27
|
type: options.type,
|
|
25
28
|
title: options.title,
|
|
26
29
|
nodeId: options.nodeId ?? undefined,
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
|
|
2
|
+
import { buildCliWriteActor } from "../daemon/write-actor.js";
|
|
2
3
|
import { runSyncCommand } from "../operations/write.js";
|
|
3
4
|
import { writeJson } from "../utils/output.js";
|
|
4
5
|
export function registerSyncCommand(program) {
|
|
@@ -24,7 +25,9 @@ export function registerSyncCommand(program) {
|
|
|
24
25
|
endpoint,
|
|
25
26
|
method: "POST",
|
|
26
27
|
path: "/sync",
|
|
28
|
+
timeoutMs: 310_000,
|
|
27
29
|
body: {
|
|
30
|
+
actor: buildCliWriteActor(process.env),
|
|
28
31
|
path: options.path ?? undefined,
|
|
29
32
|
force: options.force === true,
|
|
30
33
|
skipEmbedding: options.skipEmbedding === true,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
|
|
2
|
+
import { buildCliWriteActor } from "../daemon/write-actor.js";
|
|
2
3
|
import { renderTemplateLintResult, runTemplateLint } from "../operations/template-lint.js";
|
|
3
4
|
import { createTemplate, listTemplates, showTemplate } from "../operations/type-template.js";
|
|
4
5
|
import { ensureTextOrJson, writeJson, writeText } from "../utils/output.js";
|
|
@@ -90,7 +91,9 @@ export function registerTemplateCommand(program) {
|
|
|
90
91
|
endpoint,
|
|
91
92
|
method: "POST",
|
|
92
93
|
path: "/template/create",
|
|
94
|
+
timeoutMs: 310_000,
|
|
93
95
|
body: {
|
|
96
|
+
actor: buildCliWriteActor(process.env),
|
|
94
97
|
type: options.type,
|
|
95
98
|
title: options.title,
|
|
96
99
|
},
|
package/dist/core/db.js
CHANGED
|
@@ -149,6 +149,25 @@ function ensureBaseTables(db, embeddingDimensions) {
|
|
|
149
149
|
key TEXT PRIMARY KEY,
|
|
150
150
|
value TEXT
|
|
151
151
|
);
|
|
152
|
+
|
|
153
|
+
CREATE TABLE IF NOT EXISTS daemon_write_jobs (
|
|
154
|
+
job_id TEXT PRIMARY KEY,
|
|
155
|
+
task_type TEXT NOT NULL,
|
|
156
|
+
status TEXT NOT NULL,
|
|
157
|
+
enqueued_at TEXT NOT NULL,
|
|
158
|
+
started_at TEXT,
|
|
159
|
+
finished_at TEXT,
|
|
160
|
+
duration_ms INTEGER,
|
|
161
|
+
timeout_ms INTEGER NOT NULL,
|
|
162
|
+
queue_depth_at_enqueue INTEGER NOT NULL DEFAULT 0,
|
|
163
|
+
result_summary TEXT,
|
|
164
|
+
error_message TEXT,
|
|
165
|
+
error_details TEXT
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_daemon_write_jobs_status ON daemon_write_jobs(status);
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_daemon_write_jobs_enqueued_at ON daemon_write_jobs(enqueued_at DESC);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_daemon_write_jobs_finished_at ON daemon_write_jobs(finished_at DESC);
|
|
152
171
|
`);
|
|
153
172
|
ensureTableColumns(db, "vault_processing_queue", {
|
|
154
173
|
claimed_at: "TEXT",
|
|
@@ -191,6 +210,16 @@ export function getMeta(db, key) {
|
|
|
191
210
|
const row = db.prepare("SELECT value FROM sync_meta WHERE key = ?").get(key);
|
|
192
211
|
return row?.value ?? null;
|
|
193
212
|
}
|
|
213
|
+
export function getVectorTableDimensions(db) {
|
|
214
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE name = 'vec_pages'").get();
|
|
215
|
+
const sql = row?.sql ?? "";
|
|
216
|
+
const match = sql.match(/embedding\s+float\[(\d+)\]/i);
|
|
217
|
+
if (!match) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const dimensions = Number.parseInt(match[1], 10);
|
|
221
|
+
return Number.isFinite(dimensions) && dimensions > 0 ? dimensions : null;
|
|
222
|
+
}
|
|
194
223
|
export function setMeta(db, key, value) {
|
|
195
224
|
if (value === null) {
|
|
196
225
|
db.prepare("DELETE FROM sync_meta WHERE key = ?").run(key);
|
|
@@ -276,6 +305,8 @@ export function openDb(dbPath, config, embeddingDimensions) {
|
|
|
276
305
|
sqliteVec.load(db);
|
|
277
306
|
ensureBaseTables(db, embeddingDimensions);
|
|
278
307
|
ensureFtsTable(db);
|
|
308
|
+
const vectorDimensions = getVectorTableDimensions(db);
|
|
309
|
+
const vectorDimensionsChanged = vectorDimensions !== null && vectorDimensions !== embeddingDimensions;
|
|
279
310
|
const storedSchemaVersion = getMeta(db, "schema_version");
|
|
280
311
|
if (storedSchemaVersion && storedSchemaVersion !== SCHEMA_VERSION) {
|
|
281
312
|
db.close();
|
|
@@ -288,5 +319,5 @@ export function openDb(dbPath, config, embeddingDimensions) {
|
|
|
288
319
|
schema_version: SCHEMA_VERSION,
|
|
289
320
|
...(storedConfigVersion === null ? { config_version: config.configVersion } : {}),
|
|
290
321
|
});
|
|
291
|
-
return { db, configChanged };
|
|
322
|
+
return { db, configChanged, vectorDimensions, vectorDimensionsChanged };
|
|
292
323
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { parsePage } from "./frontmatter.js";
|
|
2
|
+
import { normalizePageId, resolvePagePath } from "./paths.js";
|
|
3
|
+
import { pathExistsSync, readTextFileSync, sha256Text } from "../utils/fs.js";
|
|
4
|
+
export function buildPageRevision(rawMarkdown) {
|
|
5
|
+
if (rawMarkdown === null) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return sha256Text(rawMarkdown);
|
|
9
|
+
}
|
|
10
|
+
export function readCanonicalPageSource(filePath, wikiPath, config) {
|
|
11
|
+
const pageId = normalizePageId(filePath, wikiPath);
|
|
12
|
+
const rawMarkdown = pathExistsSync(filePath) ? readTextFileSync(filePath) : null;
|
|
13
|
+
const parsed = rawMarkdown === null ? null : parsePage(filePath, wikiPath, config);
|
|
14
|
+
return {
|
|
15
|
+
pageId,
|
|
16
|
+
pagePath: filePath,
|
|
17
|
+
rawMarkdown,
|
|
18
|
+
frontmatter: parsed?.ok ? parsed.parsed.rawData : {},
|
|
19
|
+
revision: buildPageRevision(rawMarkdown),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function readCanonicalPageSourceById(pageId, wikiPath, config) {
|
|
23
|
+
const canonicalPageId = normalizePageId(pageId, wikiPath);
|
|
24
|
+
return readCanonicalPageSource(resolvePagePath(canonicalPageId, wikiPath), wikiPath, config);
|
|
25
|
+
}
|
package/dist/core/paths.js
CHANGED
|
@@ -155,6 +155,7 @@ export function resolveRuntimePaths(env = process.env) {
|
|
|
155
155
|
daemonPidPath: path.join(wikiRoot, ".wiki-daemon.pid"),
|
|
156
156
|
daemonLogPath: path.join(wikiRoot, ".wiki-daemon.log"),
|
|
157
157
|
daemonStatePath: path.join(wikiRoot, ".wiki-daemon.state.json"),
|
|
158
|
+
auditLogPath: path.join(wikiRoot, "..", ".wiki-runtime", "audit.ndjson"),
|
|
158
159
|
};
|
|
159
160
|
}
|
|
160
161
|
export function normalizePageId(inputPath, wikiPath) {
|
package/dist/core/runtime.js
CHANGED
|
@@ -15,6 +15,6 @@ export function loadRuntimeConfig(env = process.env) {
|
|
|
15
15
|
export function openRuntimeDb(env = process.env) {
|
|
16
16
|
const { paths, config } = loadRuntimeConfig(env);
|
|
17
17
|
const embeddingClient = EmbeddingClient.fromEnv(env);
|
|
18
|
-
const { db } = openDb(paths.dbPath, config, embeddingClient?.settings.dimensions ?? getEmbeddingDimensionFromEnv(env));
|
|
19
|
-
return { db, paths, config, embeddingClient };
|
|
18
|
+
const { db, vectorDimensions, vectorDimensionsChanged } = openDb(paths.dbPath, config, embeddingClient?.settings.dimensions ?? getEmbeddingDimensionFromEnv(env));
|
|
19
|
+
return { db, paths, config, embeddingClient, vectorDimensions, vectorDimensionsChanged };
|
|
20
20
|
}
|
package/dist/core/sync.js
CHANGED
|
@@ -78,17 +78,18 @@ export async function syncWorkspace(options = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
const config = loadConfig(runtimePaths.configPath);
|
|
80
80
|
const embeddingClient = EmbeddingClient.fromEnv(env);
|
|
81
|
-
const { db, configChanged } = openDb(runtimePaths.dbPath, config, embeddingClient?.settings.dimensions ?? getEmbeddingDimension(env));
|
|
81
|
+
const { db, configChanged, vectorDimensionsChanged } = openDb(runtimePaths.dbPath, config, embeddingClient?.settings.dimensions ?? getEmbeddingDimension(env));
|
|
82
82
|
try {
|
|
83
83
|
let mode = options.targetPaths && options.targetPaths.length > 0 && !options.force ? "path" : "full";
|
|
84
84
|
let upgradedToFullSync = false;
|
|
85
85
|
const storedEmbeddingProfile = embeddingClient ? getMeta(db, "embedding_profile") : null;
|
|
86
86
|
const profileChanged = Boolean(embeddingClient && storedEmbeddingProfile && storedEmbeddingProfile !== embeddingClient.profileHash);
|
|
87
|
-
|
|
87
|
+
const embeddingDrift = profileChanged || vectorDimensionsChanged;
|
|
88
|
+
if (mode === "path" && (configChanged || embeddingDrift)) {
|
|
88
89
|
mode = "full";
|
|
89
90
|
upgradedToFullSync = true;
|
|
90
91
|
}
|
|
91
|
-
if (
|
|
92
|
+
if (embeddingDrift && options.skipEmbedding) {
|
|
92
93
|
throw new AppError("Embedding profile changed, cannot skip embedding.", "config");
|
|
93
94
|
}
|
|
94
95
|
if (options.force) {
|
|
@@ -112,7 +113,7 @@ export async function syncWorkspace(options = {}) {
|
|
|
112
113
|
});
|
|
113
114
|
}
|
|
114
115
|
let embedAll = false;
|
|
115
|
-
if (embeddingClient &&
|
|
116
|
+
if (embeddingClient && embeddingDrift) {
|
|
116
117
|
resetVectorTable(db, embeddingClient.settings.dimensions);
|
|
117
118
|
db.prepare("UPDATE pages SET embedding_status = 'pending'").run();
|
|
118
119
|
embedAll = true;
|
|
@@ -160,7 +161,7 @@ export async function syncWorkspace(options = {}) {
|
|
|
160
161
|
mode,
|
|
161
162
|
upgradedToFullSync,
|
|
162
163
|
configChanged,
|
|
163
|
-
profileChanged,
|
|
164
|
+
profileChanged: embeddingDrift,
|
|
164
165
|
inserted: applyResult.inserted.length,
|
|
165
166
|
updated: applyResult.updated.length,
|
|
166
167
|
deleted: applyResult.deleted.length,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ensureDirSync } from "../utils/fs.js";
|
|
5
|
+
import { toOffsetIso } from "../utils/time.js";
|
|
6
|
+
export function appendAuditEvent(paths, actor, input) {
|
|
7
|
+
ensureDirSync(path.dirname(paths.auditLogPath));
|
|
8
|
+
const event = {
|
|
9
|
+
eventId: randomUUID(),
|
|
10
|
+
timestamp: toOffsetIso(),
|
|
11
|
+
requestId: actor.requestId,
|
|
12
|
+
actorId: actor.actorId,
|
|
13
|
+
actorType: actor.actorType,
|
|
14
|
+
...input,
|
|
15
|
+
};
|
|
16
|
+
appendFileSync(paths.auditLogPath, `${JSON.stringify(event)}\n`, "utf8");
|
|
17
|
+
return event;
|
|
18
|
+
}
|
package/dist/daemon/client.js
CHANGED
|
@@ -124,7 +124,7 @@ export async function requestDaemonJson(options) {
|
|
|
124
124
|
method: options.method,
|
|
125
125
|
headers: options.body === undefined ? undefined : { "content-type": "application/json" },
|
|
126
126
|
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
127
|
-
}, 5_000);
|
|
127
|
+
}, options.timeoutMs ?? (options.method === "POST" ? 310_000 : 5_000));
|
|
128
128
|
}
|
|
129
129
|
function warnReadFallback(availability) {
|
|
130
130
|
const location = availability.state
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathExistsSync } from "../utils/fs.js";
|
|
4
|
+
import { AppError } from "../utils/errors.js";
|
|
5
|
+
function buildGitEnv(actor) {
|
|
6
|
+
const emailLocalPart = actor.actorId.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
7
|
+
return {
|
|
8
|
+
...process.env,
|
|
9
|
+
GIT_AUTHOR_NAME: actor.actorId,
|
|
10
|
+
GIT_AUTHOR_EMAIL: `${emailLocalPart}@tiangong-wiki.local`,
|
|
11
|
+
GIT_COMMITTER_NAME: actor.actorId,
|
|
12
|
+
GIT_COMMITTER_EMAIL: `${emailLocalPart}@tiangong-wiki.local`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function runGit(paths, actor, args) {
|
|
16
|
+
const result = spawnSync("git", ["-C", paths.wikiRoot, ...args], {
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
env: buildGitEnv(actor),
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
status: result.status,
|
|
22
|
+
stdout: result.stdout ?? "",
|
|
23
|
+
stderr: result.stderr ?? "",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function collectStageTargets(paths) {
|
|
27
|
+
return [
|
|
28
|
+
path.relative(paths.wikiRoot, paths.wikiPath),
|
|
29
|
+
path.relative(paths.wikiRoot, paths.dbPath),
|
|
30
|
+
path.relative(paths.wikiRoot, paths.templatesPath),
|
|
31
|
+
path.relative(paths.wikiRoot, paths.configPath),
|
|
32
|
+
path.relative(paths.wikiRoot, paths.queueArtifactsPath),
|
|
33
|
+
].filter((entry, index, values) => entry && !entry.startsWith("..") && values.indexOf(entry) === index && pathExistsSync(path.join(paths.wikiRoot, entry)));
|
|
34
|
+
}
|
|
35
|
+
export function commitWriteJournal(paths, actor, input) {
|
|
36
|
+
const gitRoot = runGit(paths, actor, ["rev-parse", "--show-toplevel"]);
|
|
37
|
+
if (gitRoot.status !== 0) {
|
|
38
|
+
throw new AppError("Git repository not initialized for wiki root.", "runtime", {
|
|
39
|
+
code: "git_commit_failed",
|
|
40
|
+
stderr: gitRoot.stderr.trim() || gitRoot.stdout.trim(),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const targets = collectStageTargets(paths);
|
|
44
|
+
const addResult = runGit(paths, actor, ["add", "-A", "--", ...targets]);
|
|
45
|
+
if (addResult.status !== 0) {
|
|
46
|
+
throw new AppError("Failed to stage wiki changes for Git commit.", "runtime", {
|
|
47
|
+
code: "git_commit_failed",
|
|
48
|
+
stderr: addResult.stderr.trim() || addResult.stdout.trim(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const diffResult = runGit(paths, actor, ["diff", "--cached", "--quiet", "--exit-code"]);
|
|
52
|
+
if (diffResult.status === 0) {
|
|
53
|
+
return {
|
|
54
|
+
status: "no_changes",
|
|
55
|
+
commitHash: null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (diffResult.status !== 1) {
|
|
59
|
+
throw new AppError("Failed to inspect staged Git changes.", "runtime", {
|
|
60
|
+
code: "git_commit_failed",
|
|
61
|
+
stderr: diffResult.stderr.trim() || diffResult.stdout.trim(),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const commitMessage = `wiki: ${input.operation} ${input.resourceId ?? "*"} by ${actor.actorId}`;
|
|
65
|
+
const commitResult = runGit(paths, actor, ["commit", "-m", commitMessage]);
|
|
66
|
+
if (commitResult.status !== 0) {
|
|
67
|
+
throw new AppError("Failed to create Git journal commit.", "runtime", {
|
|
68
|
+
code: "git_commit_failed",
|
|
69
|
+
stderr: commitResult.stderr.trim() || commitResult.stdout.trim(),
|
|
70
|
+
commitMessage,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const hashResult = runGit(paths, actor, ["rev-parse", "HEAD"]);
|
|
74
|
+
if (hashResult.status !== 0) {
|
|
75
|
+
throw new AppError("Failed to resolve Git commit hash.", "runtime", {
|
|
76
|
+
code: "git_commit_failed",
|
|
77
|
+
stderr: hashResult.stderr.trim() || hashResult.stdout.trim(),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
status: "committed",
|
|
82
|
+
commitHash: hashResult.stdout.trim(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export class GitPushScheduler {
|
|
86
|
+
paths;
|
|
87
|
+
log;
|
|
88
|
+
enabled;
|
|
89
|
+
delayMs;
|
|
90
|
+
remote;
|
|
91
|
+
timer = null;
|
|
92
|
+
constructor(paths, log, env = process.env) {
|
|
93
|
+
this.paths = paths;
|
|
94
|
+
this.log = log;
|
|
95
|
+
this.enabled = (env.WIKI_GIT_AUTO_PUSH ?? "").trim().toLowerCase() === "true";
|
|
96
|
+
this.delayMs = Number.parseInt(env.WIKI_GIT_PUSH_DELAY_MS ?? "3000", 10) || 3000;
|
|
97
|
+
this.remote = env.WIKI_GIT_PUSH_REMOTE?.trim() || "origin";
|
|
98
|
+
}
|
|
99
|
+
schedule(actor) {
|
|
100
|
+
if (!this.enabled || this.timer) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
this.timer = setTimeout(() => {
|
|
104
|
+
this.timer = null;
|
|
105
|
+
const result = runGit(this.paths, actor, ["push", this.remote, "HEAD"]);
|
|
106
|
+
if (result.status === 0) {
|
|
107
|
+
this.log(`git push ok remote=${this.remote}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.log(`git push failed remote=${this.remote}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
111
|
+
}, this.delayMs);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|