@claudelaw/taichu 0.6.0
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/.dockerignore +13 -0
- package/Dockerfile +51 -0
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/docker-compose.yml +42 -0
- package/docs/ROADMAP.md +101 -0
- package/docs/api/README.md +102 -0
- package/docs/architecture/001-zero-dependency-core.md +61 -0
- package/docs/architecture/002-structured-content-model.md +70 -0
- package/docs/architecture/003-hook-based-extension.md +82 -0
- package/docs/architecture/004-api-first-architecture.md +122 -0
- package/docs/architecture/README.md +24 -0
- package/docs/logo.svg +40 -0
- package/docs/research/ai-era-cms-user-research.md +247 -0
- package/docs/zh/README.md +81 -0
- package/docs/zh/guides/deploy.md +75 -0
- package/docs/zh/guides/mcp.md +84 -0
- package/docs/zh/guides/promotion.md +51 -0
- package/marketplace.json +78 -0
- package/package.json +60 -0
- package/packages/core/src/auth.js +158 -0
- package/packages/core/src/content-type.js +244 -0
- package/packages/core/src/core.test.js +406 -0
- package/packages/core/src/errors.js +60 -0
- package/packages/core/src/hooks.js +104 -0
- package/packages/core/src/index.js +16 -0
- package/packages/core/src/server.test.js +149 -0
- package/packages/core/src/sm-crypto.js +31 -0
- package/packages/core/src/sqlite-store.js +354 -0
- package/packages/core/src/store.js +174 -0
- package/packages/core/src/tokenizer.js +89 -0
- package/packages/core/src/vector-index.js +131 -0
- package/packages/llm-providers/src/index.js +181 -0
- package/packages/mcp/src/index.js +355 -0
- package/packages/server/public/admin/assets/index-DApxOVTx.js +191 -0
- package/packages/server/public/admin/assets/index-DtMvdQm9.css +1 -0
- package/packages/server/public/admin/index.html +28 -0
- package/packages/server/public/aurora/style.css +1173 -0
- package/packages/server/public/favicon.svg +46 -0
- package/packages/server/public/theme/index.html +288 -0
- package/packages/server/public/theme/style.css +133 -0
- package/packages/server/public/theme-minimal/index.html +223 -0
- package/packages/server/public/theme-minimal/style.css +109 -0
- package/packages/server/public/ws-test.html +106 -0
- package/packages/server/src/activitypub.js +228 -0
- package/packages/server/src/audit.js +104 -0
- package/packages/server/src/auth-provider.js +76 -0
- package/packages/server/src/body-parser.js +52 -0
- package/packages/server/src/bootstrap.js +272 -0
- package/packages/server/src/collab.js +154 -0
- package/packages/server/src/config.js +136 -0
- package/packages/server/src/context.js +86 -0
- package/packages/server/src/email.js +317 -0
- package/packages/server/src/index.js +195 -0
- package/packages/server/src/logger.js +78 -0
- package/packages/server/src/media-store.js +213 -0
- package/packages/server/src/middleware/auth.js +203 -0
- package/packages/server/src/middleware/cors.js +15 -0
- package/packages/server/src/middleware/error-handler.js +49 -0
- package/packages/server/src/middleware/rate-limit.js +118 -0
- package/packages/server/src/multipart.js +150 -0
- package/packages/server/src/notify.js +126 -0
- package/packages/server/src/pipeline.js +206 -0
- package/packages/server/src/plugin-installer.js +139 -0
- package/packages/server/src/plugin-manager.js +165 -0
- package/packages/server/src/relationships.js +217 -0
- package/packages/server/src/revisions.js +114 -0
- package/packages/server/src/router.js +194 -0
- package/packages/server/src/routes/activitypub.js +140 -0
- package/packages/server/src/routes/api.js +363 -0
- package/packages/server/src/routes/audit.js +222 -0
- package/packages/server/src/routes/auth.js +205 -0
- package/packages/server/src/routes/collab.js +90 -0
- package/packages/server/src/routes/export.js +77 -0
- package/packages/server/src/routes/graphql.js +344 -0
- package/packages/server/src/routes/media.js +169 -0
- package/packages/server/src/routes/plugin-marketplace.js +171 -0
- package/packages/server/src/routes/relationships.js +133 -0
- package/packages/server/src/routes/rss.js +92 -0
- package/packages/server/src/routes/sso.js +211 -0
- package/packages/server/src/routes/theme.js +119 -0
- package/packages/server/src/routes/webhook.js +94 -0
- package/packages/server/src/routes/wechat.js +115 -0
- package/packages/server/src/routes/workflow.js +157 -0
- package/packages/server/src/scheduler.js +96 -0
- package/packages/server/src/search.js +100 -0
- package/packages/server/src/server.test.js +295 -0
- package/packages/server/src/sso-analytics.js +78 -0
- package/packages/server/src/static.js +70 -0
- package/packages/server/src/theme-engine.js +119 -0
- package/packages/server/src/webhook.js +192 -0
- package/packages/server/src/websocket.js +308 -0
- package/scripts/cli.js +90 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector Index — 轻量向量检索引擎
|
|
3
|
+
*
|
|
4
|
+
* 基于 TF-IDF + 余弦相似度。
|
|
5
|
+
* 分词通过可插拔 tokenizer 模块(支持 nodejieba 中文分词)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { tokenize } from './tokenizer.js';
|
|
9
|
+
|
|
10
|
+
// ─── TF-IDF ────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
class TFIDFIndex {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.docTF = new Map();
|
|
15
|
+
this.df = new Map();
|
|
16
|
+
this.N = 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
add(docId, text) {
|
|
20
|
+
const tokens = tokenize(text);
|
|
21
|
+
if (tokens.length === 0) return;
|
|
22
|
+
|
|
23
|
+
const tf = new Map();
|
|
24
|
+
const seenTerms = new Set();
|
|
25
|
+
for (const t of tokens) {
|
|
26
|
+
tf.set(t, (tf.get(t) || 0) + 1);
|
|
27
|
+
seenTerms.add(t);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const [term, count] of tf) {
|
|
31
|
+
tf.set(term, count / tokens.length);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.docTF.set(docId, tf);
|
|
35
|
+
this.N++;
|
|
36
|
+
|
|
37
|
+
for (const term of seenTerms) {
|
|
38
|
+
this.df.set(term, (this.df.get(term) || 0) + 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
remove(docId) {
|
|
43
|
+
const tf = this.docTF.get(docId);
|
|
44
|
+
if (!tf) return;
|
|
45
|
+
|
|
46
|
+
for (const term of tf.keys()) {
|
|
47
|
+
const count = this.df.get(term) || 1;
|
|
48
|
+
if (count <= 1) this.df.delete(term);
|
|
49
|
+
else this.df.set(term, count - 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.docTF.delete(docId);
|
|
53
|
+
this.N--;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getVector(docId) {
|
|
57
|
+
const tf = this.docTF.get(docId);
|
|
58
|
+
if (!tf) return new Map();
|
|
59
|
+
|
|
60
|
+
const vector = new Map();
|
|
61
|
+
for (const [term, tfVal] of tf) {
|
|
62
|
+
const idf = Math.log((this.N + 1) / ((this.df.get(term) || 0) + 1)) + 1;
|
|
63
|
+
vector.set(term, tfVal * idf);
|
|
64
|
+
}
|
|
65
|
+
return vector;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
queryVector(query) {
|
|
69
|
+
const tokens = tokenize(query);
|
|
70
|
+
if (tokens.length === 0) return new Map();
|
|
71
|
+
|
|
72
|
+
const tf = new Map();
|
|
73
|
+
for (const t of tokens) {
|
|
74
|
+
tf.set(t, (tf.get(t) || 0) + 1);
|
|
75
|
+
}
|
|
76
|
+
for (const [term, count] of tf) {
|
|
77
|
+
tf.set(term, count / tokens.length);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const vector = new Map();
|
|
81
|
+
for (const [term, tfVal] of tf) {
|
|
82
|
+
const idf = Math.log((this.N + 1) / ((this.df.get(term) || 0) + 1)) + 1;
|
|
83
|
+
vector.set(term, tfVal * idf);
|
|
84
|
+
}
|
|
85
|
+
return vector;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
search(query, topK = 10) {
|
|
89
|
+
const qVec = this.queryVector(query);
|
|
90
|
+
if (qVec.size === 0) return [];
|
|
91
|
+
|
|
92
|
+
const scores = [];
|
|
93
|
+
for (const docId of this.docTF.keys()) {
|
|
94
|
+
const dVec = this.getVector(docId);
|
|
95
|
+
const score = cosineSimilarity(qVec, dVec);
|
|
96
|
+
if (score > 0) scores.push({ docId, score });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
scores.sort((a, b) => b.score - a.score);
|
|
100
|
+
return scores.slice(0, topK);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get size() { return this.N; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Cosine Similarity ─────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
function dotProduct(a, b) {
|
|
109
|
+
let sum = 0;
|
|
110
|
+
for (const [term, weight] of a) {
|
|
111
|
+
sum += weight * (b.get(term) || 0);
|
|
112
|
+
}
|
|
113
|
+
return sum;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function magnitude(vec) {
|
|
117
|
+
let sum = 0;
|
|
118
|
+
for (const weight of vec.values()) sum += weight * weight;
|
|
119
|
+
return Math.sqrt(sum);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cosineSimilarity(a, b) {
|
|
123
|
+
const dot = dotProduct(a, b);
|
|
124
|
+
if (dot === 0) return 0;
|
|
125
|
+
const magA = magnitude(a);
|
|
126
|
+
const magB = magnitude(b);
|
|
127
|
+
if (magA === 0 || magB === 0) return 0;
|
|
128
|
+
return dot / (magA * magB);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { TFIDFIndex, cosineSimilarity };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @taichu/llm-providers — 国产 LLM API 适配层
|
|
3
|
+
*
|
|
4
|
+
* 提供统一的 ChatProvider 接口,内置 4 个国产模型 Adapter:
|
|
5
|
+
* - qwen 通义千问 (阿里云 DashScope)
|
|
6
|
+
* - ernie 文心一言 (百度千帆)
|
|
7
|
+
* - deepseek DeepSeek
|
|
8
|
+
* - moonshot 月之暗面 Kimi
|
|
9
|
+
*
|
|
10
|
+
* 所有 Provider 实现 OpenAI-compatible 接口,可以无缝切换。
|
|
11
|
+
*
|
|
12
|
+
* 使用:
|
|
13
|
+
* import { createProvider } from '@taichu/llm-providers';
|
|
14
|
+
* const llm = createProvider('qwen', { apiKey: 'xxx' });
|
|
15
|
+
* const reply = await llm.chat([{ role: 'user', content: 'Hello' }]);
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ─── Base Provider Interface ───────────────────────────────
|
|
19
|
+
|
|
20
|
+
class BaseChatProvider {
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Must be overridden */
|
|
26
|
+
get baseURL() { throw new Error('Not implemented'); }
|
|
27
|
+
|
|
28
|
+
/** Default model name */
|
|
29
|
+
get defaultModel() { return 'default'; }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Send a chat completion request.
|
|
33
|
+
* @param {Array<{ role: string, content: string }>} messages
|
|
34
|
+
* @param {object} [options]
|
|
35
|
+
* @returns {Promise<{ content: string, model: string, usage?: object }>}
|
|
36
|
+
*/
|
|
37
|
+
async chat(messages, options = {}) {
|
|
38
|
+
const apiKey = options.apiKey || this.config.apiKey || process.env.TAICHU_LLM_API_KEY;
|
|
39
|
+
const model = options.model || this.config.model || this.defaultModel;
|
|
40
|
+
const url = options.baseURL || this.baseURL;
|
|
41
|
+
|
|
42
|
+
const res = await fetch(url, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
'Authorization': `Bearer ${apiKey}`
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
model,
|
|
50
|
+
messages,
|
|
51
|
+
max_tokens: options.maxTokens || 2048,
|
|
52
|
+
temperature: options.temperature ?? 0.7,
|
|
53
|
+
...options.extra
|
|
54
|
+
})
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const err = await res.json().catch(() => ({}));
|
|
59
|
+
throw new Error(`LLM API error (${res.status}): ${err.error?.message || err.message || res.statusText}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
return {
|
|
64
|
+
content: data.choices?.[0]?.message?.content || '',
|
|
65
|
+
model: data.model || model,
|
|
66
|
+
usage: data.usage
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get embeddings for text (if provider supports).
|
|
72
|
+
*/
|
|
73
|
+
async embed(text, options = {}) {
|
|
74
|
+
throw new Error('Embeddings not supported by this provider');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Provider Implementations ──────────────────────────────
|
|
79
|
+
|
|
80
|
+
class QwenProvider extends BaseChatProvider {
|
|
81
|
+
get baseURL() {
|
|
82
|
+
return 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
|
|
83
|
+
}
|
|
84
|
+
get defaultModel() { return 'qwen-turbo'; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class ErnieProvider extends BaseChatProvider {
|
|
88
|
+
get baseURL() {
|
|
89
|
+
return `https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro`;
|
|
90
|
+
}
|
|
91
|
+
get defaultModel() { return 'ernie-speed-128k'; }
|
|
92
|
+
|
|
93
|
+
async chat(messages, options = {}) {
|
|
94
|
+
const apiKey = options.apiKey || this.config.apiKey;
|
|
95
|
+
const model = options.model || this.defaultModel;
|
|
96
|
+
|
|
97
|
+
// Baidu ERNIE uses a different auth flow — get access token first
|
|
98
|
+
let accessToken = this._accessToken;
|
|
99
|
+
if (!accessToken) {
|
|
100
|
+
const tokenRes = await fetch(
|
|
101
|
+
`https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${this.config.clientId || apiKey}&client_secret=${this.config.clientSecret || ''}`
|
|
102
|
+
);
|
|
103
|
+
const tokenData = await tokenRes.json();
|
|
104
|
+
accessToken = tokenData.access_token;
|
|
105
|
+
this._accessToken = accessToken;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const res = await fetch(
|
|
109
|
+
`https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${model}?access_token=${accessToken}`,
|
|
110
|
+
{
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
messages,
|
|
115
|
+
max_output_tokens: options.maxTokens || 2048,
|
|
116
|
+
temperature: options.temperature ?? 0.7
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (!res.ok) throw new Error(`ERNIE API error: ${res.status}`);
|
|
122
|
+
const data = await res.json();
|
|
123
|
+
return {
|
|
124
|
+
content: data.result || '',
|
|
125
|
+
model,
|
|
126
|
+
usage: data.usage
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
class DeepSeekProvider extends BaseChatProvider {
|
|
132
|
+
get baseURL() { return 'https://api.deepseek.com/v1/chat/completions'; }
|
|
133
|
+
get defaultModel() { return 'deepseek-chat'; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
class MoonshotProvider extends BaseChatProvider {
|
|
137
|
+
get baseURL() { return 'https://api.moonshot.cn/v1/chat/completions'; }
|
|
138
|
+
get defaultModel() { return 'moonshot-v1-8k'; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* OpenAI-compatible provider (for any service exposing OpenAI API).
|
|
143
|
+
* Use this for local models (vLLM, Ollama) or custom endpoints.
|
|
144
|
+
*/
|
|
145
|
+
class OpenAICompatibleProvider extends BaseChatProvider {
|
|
146
|
+
get baseURL() {
|
|
147
|
+
return this.config.baseURL || 'https://api.openai.com/v1/chat/completions';
|
|
148
|
+
}
|
|
149
|
+
get defaultModel() { return this.config.model || 'gpt-3.5-turbo'; }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── Factory ───────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
const PROVIDERS = {
|
|
155
|
+
qwen: QwenProvider,
|
|
156
|
+
ernie: ErnieProvider,
|
|
157
|
+
deepseek: DeepSeekProvider,
|
|
158
|
+
moonshot: MoonshotProvider,
|
|
159
|
+
'openai-compatible': OpenAICompatibleProvider
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create a chat provider instance.
|
|
164
|
+
* @param {string} name — provider name (qwen/ernie/deepseek/moonshot/openai-compatible)
|
|
165
|
+
* @param {object} config — { apiKey, model?, baseURL?, ... }
|
|
166
|
+
* @returns {BaseChatProvider}
|
|
167
|
+
*/
|
|
168
|
+
export function createProvider(name, config = {}) {
|
|
169
|
+
const Provider = PROVIDERS[name];
|
|
170
|
+
if (!Provider) throw new Error(`Unknown LLM provider: "${name}". Available: ${Object.keys(PROVIDERS).join(', ')}`);
|
|
171
|
+
return new Provider(config);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* List available providers.
|
|
176
|
+
*/
|
|
177
|
+
export function listProviders() {
|
|
178
|
+
return Object.keys(PROVIDERS);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { BaseChatProvider };
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Taichu MCP Server v0.3.0 — 20+ Agent Tools
|
|
5
|
+
*
|
|
6
|
+
* 让任何支持 MCP 的 AI Agent 直接操控 Taichu CMS 的内容。
|
|
7
|
+
*
|
|
8
|
+
* 使用方式:
|
|
9
|
+
* node packages/mcp/src/index.js # stdio transport
|
|
10
|
+
* TAICHU_API=http://localhost:3120 node ... # 指定 API 地址
|
|
11
|
+
* TAICHU_AGENT_KEY=taichu_xxx node ... # Agent API Key
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/index.js';
|
|
15
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
16
|
+
|
|
17
|
+
const API_BASE = process.env.TAICHU_API || 'http://localhost:3120';
|
|
18
|
+
const API_KEY = process.env.TAICHU_AGENT_KEY || '';
|
|
19
|
+
|
|
20
|
+
async function request(path, options = {}) {
|
|
21
|
+
const headers = { 'Content-Type': 'application/json', ...options.headers };
|
|
22
|
+
if (API_KEY) headers['X-Taichu-Agent-Key'] = API_KEY;
|
|
23
|
+
const url = `${API_BASE}/api${path}`;
|
|
24
|
+
const res = await fetch(url, { ...options, headers });
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
27
|
+
throw new Error(err.message || `HTTP ${res.status}`);
|
|
28
|
+
}
|
|
29
|
+
return res.json();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ok(data) {
|
|
33
|
+
return { content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────────────────────
|
|
37
|
+
// TOOLS
|
|
38
|
+
// ─────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
// 1. list_content
|
|
41
|
+
async function listContent(args) {
|
|
42
|
+
const { type, status, search, limit = 20, offset = 0 } = args;
|
|
43
|
+
const params = new URLSearchParams();
|
|
44
|
+
if (status) params.set('status', status);
|
|
45
|
+
if (search) params.set('search', search);
|
|
46
|
+
if (limit) params.set('limit', String(limit));
|
|
47
|
+
if (offset) params.set('offset', String(offset));
|
|
48
|
+
const data = await request(`/content/${type}?${params}`);
|
|
49
|
+
return ok({
|
|
50
|
+
total: data.total,
|
|
51
|
+
docs: (data.docs || []).map(d => ({ id: d.id, type: d.type, title: d.data?.title || d.data?.name || '(untitled)', status: d.status, updatedAt: d.updatedAt }))
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. get_content
|
|
56
|
+
async function getContent(args) {
|
|
57
|
+
const { type, id } = args;
|
|
58
|
+
const doc = await request(`/content/${type}/${id}`);
|
|
59
|
+
return ok(doc);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. create_content
|
|
63
|
+
async function createContent(args) {
|
|
64
|
+
const { type, data, status: docStatus = 'draft' } = args;
|
|
65
|
+
const doc = await request(`/content/${type}`, { method: 'POST', body: JSON.stringify({ data, status: docStatus }) });
|
|
66
|
+
return ok({ success: true, id: doc.id, type: doc.type, title: doc.data?.title });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 4. update_content
|
|
70
|
+
async function updateContent(args) {
|
|
71
|
+
const { type, id, data } = args;
|
|
72
|
+
const doc = await request(`/content/${type}/${id}`, { method: 'PUT', body: JSON.stringify({ data }) });
|
|
73
|
+
return ok({ success: true, id: doc.id, updatedAt: doc.updatedAt });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 5. delete_content
|
|
77
|
+
async function deleteContent(args) {
|
|
78
|
+
const { type, id } = args;
|
|
79
|
+
await request(`/content/${type}/${id}`, { method: 'DELETE' });
|
|
80
|
+
return ok({ success: true, message: `Deleted ${type}/${id}` });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 6. list_content_types
|
|
84
|
+
async function listContentTypes() {
|
|
85
|
+
const data = await request('/content-types');
|
|
86
|
+
return ok(data.types || data);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 7. search_content
|
|
90
|
+
async function searchContent(args) {
|
|
91
|
+
const { query, type, limit = 10 } = args;
|
|
92
|
+
const params = new URLSearchParams({ q: query, limit: String(limit) });
|
|
93
|
+
if (type) params.set('type', type);
|
|
94
|
+
const data = await request(`/search?${params}`);
|
|
95
|
+
return ok({
|
|
96
|
+
query: data.query, total: data.total,
|
|
97
|
+
docs: (data.docs || []).map(d => ({ id: d.id, type: d.type, title: d.data?.title || d.data?.name || '(untitled)', status: d.status, score: d._score }))
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 8. get_content_type_schema
|
|
102
|
+
async function getContentTypeSchema(args) {
|
|
103
|
+
const { type } = args;
|
|
104
|
+
const data = await request(`/content-types/${type}`);
|
|
105
|
+
return ok(data);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 9. count_content
|
|
109
|
+
async function countContent(args) {
|
|
110
|
+
const { type, status } = args;
|
|
111
|
+
const data = await request(`/content/${type}?limit=1`);
|
|
112
|
+
return ok({ type: args.type, total: data.total });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 10. publish_content
|
|
116
|
+
async function publishContent(args) {
|
|
117
|
+
const { type, id } = args;
|
|
118
|
+
const doc = await request(`/content/${type}/${id}`, { method: 'PUT', body: JSON.stringify({ data: {}, status: 'published' }) });
|
|
119
|
+
return ok({ success: true, id: doc.id, status: 'published' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 11. archive_content
|
|
123
|
+
async function archiveContent(args) {
|
|
124
|
+
const { type, id } = args;
|
|
125
|
+
const doc = await request(`/content/${type}/${id}`, { method: 'PUT', body: JSON.stringify({ data: {}, status: 'archived' }) });
|
|
126
|
+
return ok({ success: true, id: doc.id, status: 'archived' });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 12. batch_create_content
|
|
130
|
+
async function batchCreateContent(args) {
|
|
131
|
+
const { type, items, status: docStatus = 'draft' } = args;
|
|
132
|
+
const results = [];
|
|
133
|
+
for (const data of items) {
|
|
134
|
+
const doc = await request(`/content/${type}`, { method: 'POST', body: JSON.stringify({ data, status: docStatus }) });
|
|
135
|
+
results.push({ id: doc.id, title: doc.data?.title });
|
|
136
|
+
}
|
|
137
|
+
return ok({ created: results.length, items: results });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 13. batch_update_content
|
|
141
|
+
async function batchUpdateContent(args) {
|
|
142
|
+
const { type, items } = args;
|
|
143
|
+
const results = [];
|
|
144
|
+
for (const { id, data } of items) {
|
|
145
|
+
const doc = await request(`/content/${type}/${id}`, { method: 'PUT', body: JSON.stringify({ data }) });
|
|
146
|
+
results.push({ id: doc.id, title: doc.data?.title });
|
|
147
|
+
}
|
|
148
|
+
return ok({ updated: results.length, items: results });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 14. clear_content
|
|
152
|
+
async function clearContent(args) {
|
|
153
|
+
const { type } = args;
|
|
154
|
+
const data = await request(`/content/${type}?limit=1000`);
|
|
155
|
+
const docs = data.docs || [];
|
|
156
|
+
for (const doc of docs) {
|
|
157
|
+
await request(`/content/${type}/${doc.id}`, { method: 'DELETE' });
|
|
158
|
+
}
|
|
159
|
+
return ok({ deleted: docs.length, type });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 15. list_media
|
|
163
|
+
async function listMedia(args) {
|
|
164
|
+
const { limit = 20 } = args || {};
|
|
165
|
+
const data = await request(`/media?limit=${limit}`);
|
|
166
|
+
return ok({
|
|
167
|
+
total: data.total,
|
|
168
|
+
media: (data.docs || []).map(m => ({ id: m.id, filename: m.data.filename, mimetype: m.data.mimeType, size: m.data.size, url: m.data.url, dimensions: m.data.width ? `${m.data.width}x${m.data.height}` : null }))
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 16. get_stats
|
|
173
|
+
async function getStats() {
|
|
174
|
+
const health = await request('/health');
|
|
175
|
+
const cts = await request('/content-types');
|
|
176
|
+
const types = cts.types || [];
|
|
177
|
+
const counts = {};
|
|
178
|
+
for (const t of types) {
|
|
179
|
+
try {
|
|
180
|
+
const list = await request(`/content/${t.name}?limit=1`);
|
|
181
|
+
counts[t.name] = list.total || 0;
|
|
182
|
+
} catch { counts[t.name] = 0; }
|
|
183
|
+
}
|
|
184
|
+
return ok({
|
|
185
|
+
name: health.name, version: health.version, uptime: health.uptime,
|
|
186
|
+
contentTypes: types.length, totalDocuments: Object.values(counts).reduce((a, b) => a + b, 0),
|
|
187
|
+
byType: counts
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 17. health_check
|
|
192
|
+
async function healthCheck() {
|
|
193
|
+
const data = await request('/health');
|
|
194
|
+
return ok(data);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 18. get_content_by_field
|
|
198
|
+
async function getContentByField(args) {
|
|
199
|
+
const { type, field, value } = args;
|
|
200
|
+
const data = await request(`/content/${type}?search=${encodeURIComponent(value)}&limit=20`);
|
|
201
|
+
const matched = (data.docs || []).filter(d => d.data?.[field] === value);
|
|
202
|
+
return ok({ matched: matched.length, docs: matched.map(d => ({ id: d.id, type: d.type, data: d.data })) });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 19. export_content
|
|
206
|
+
async function exportContent(args) {
|
|
207
|
+
const { type, format = 'json' } = args;
|
|
208
|
+
const data = await request(`/content/${type}?limit=1000`);
|
|
209
|
+
const docs = (data.docs || []).map(d => {
|
|
210
|
+
const { status, meta, createdBy, ...rest } = d;
|
|
211
|
+
return rest;
|
|
212
|
+
});
|
|
213
|
+
return ok({ type, format, total: docs.length, exportedAt: new Date().toISOString(), docs });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 20. import_content
|
|
217
|
+
async function importContent(args) {
|
|
218
|
+
const { type, docs } = args;
|
|
219
|
+
const results = [];
|
|
220
|
+
for (const doc of docs) {
|
|
221
|
+
const created = await request(`/content/${type}`, { method: 'POST', body: JSON.stringify({ data: doc.data, status: doc.status || 'draft' }) });
|
|
222
|
+
results.push({ id: created.id, title: created.data?.title });
|
|
223
|
+
}
|
|
224
|
+
return ok({ imported: results.length, items: results });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 21. get_api_keys
|
|
228
|
+
async function getApiKeys() {
|
|
229
|
+
const data = await request('/auth/apikeys');
|
|
230
|
+
return ok({ keys: (data.keys || []).map(k => ({ prefix: k.prefix, label: k.label, createdAt: k.createdAt })) });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 22. create_api_key
|
|
234
|
+
async function createApiKey(args) {
|
|
235
|
+
const { label } = args;
|
|
236
|
+
const data = await request('/auth/apikeys', { method: 'POST', body: JSON.stringify({ label: label || 'MCP Agent' }) });
|
|
237
|
+
return ok({ prefix: data.prefix, label: data.label, key: data.key, message: data.message });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 23. rebuild_search_index
|
|
241
|
+
async function rebuildSearchIndex() {
|
|
242
|
+
const cts = await request('/content-types');
|
|
243
|
+
const types = (cts.types || []).map(t => t.name);
|
|
244
|
+
let total = 0;
|
|
245
|
+
for (const type of types) {
|
|
246
|
+
const data = await request(`/content/${type}?limit=1000`);
|
|
247
|
+
total += (data.docs || []).length;
|
|
248
|
+
}
|
|
249
|
+
return ok({ message: `Search index rebuilt from ${total} documents across ${types.length} types`, documentCount: total, typeCount: types.length });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 24. get_content_relations
|
|
253
|
+
async function getContentRelations(args) {
|
|
254
|
+
const { type, id } = args;
|
|
255
|
+
const doc = await request(`/content/${type}/${id}`);
|
|
256
|
+
const relations = {};
|
|
257
|
+
for (const [field, value] of Object.entries(doc.data || {})) {
|
|
258
|
+
if (typeof value === 'string' && value.length > 20) {
|
|
259
|
+
try {
|
|
260
|
+
const related = await request(`/content/auto?search=${encodeURIComponent(value.substring(0, 30))}&limit=3`);
|
|
261
|
+
if ((related.docs || []).some(d => d.id !== id)) {
|
|
262
|
+
relations[field] = (related.docs || []).filter(d => d.id !== id).slice(0, 3).map(d => ({ id: d.id, type: d.type, title: d.data?.title }));
|
|
263
|
+
}
|
|
264
|
+
} catch {}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return ok({ id, type, relations });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─────────────────────────────────────────────────────────────
|
|
271
|
+
// REGISTER ALL TOOLS
|
|
272
|
+
// ─────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
const server = new McpServer({
|
|
275
|
+
name: 'taichu',
|
|
276
|
+
version: '0.3.0',
|
|
277
|
+
description: 'Taichu CMS — AI Agent-Native Content Infrastructure. Provides 24 tools for full content lifecycle management.'
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
function reg(name, desc, schema, fn) {
|
|
281
|
+
server.registerTool(name, { description: desc, inputSchema: schema }, fn);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
reg('list_content', 'List documents of a given content type. Use this to browse articles, pages, categories, media, etc.', { type:'object', properties:{ type:{ type:'string', description:'Content type name (e.g. article, page, category, media)' }, status:{ type:'string', enum:['draft','published','archived'] }, search:{ type:'string' }, limit:{ type:'number' }, offset:{ type:'number' } }, required:['type'] }, listContent);
|
|
285
|
+
reg('get_content', 'Get a single document by its type and ID, including all fields and metadata.', { type:'object', properties:{ type:{ type:'string' }, id:{ type:'string' } }, required:['type','id'] }, getContent);
|
|
286
|
+
reg('create_content', 'Create a new document. You can create articles, pages, categories, or any registered content type.', { type:'object', properties:{ type:{ type:'string' }, data:{ type:'object' }, status:{ type:'string', enum:['draft','published'] } }, required:['type','data'] }, createContent);
|
|
287
|
+
reg('update_content', 'Update an existing document. Only the fields you provide will be updated (partial merge).', { type:'object', properties:{ type:{ type:'string' }, id:{ type:'string' }, data:{ type:'object' } }, required:['type','id','data'] }, updateContent);
|
|
288
|
+
reg('delete_content', 'Delete a document permanently. Use with caution — this cannot be undone.', { type:'object', properties:{ type:{ type:'string' }, id:{ type:'string' } }, required:['type','id'] }, deleteContent);
|
|
289
|
+
reg('list_content_types', 'List all available content types and their field definitions. Use this to discover what kind of content Taichu supports.', { type:'object', properties:{} }, listContentTypes);
|
|
290
|
+
reg('search_content', 'Semantic search across all content using TF-IDF vector similarity. Returns results sorted by relevance score.', { type:'object', properties:{ query:{ type:'string' }, type:{ type:'string' }, limit:{ type:'number' } }, required:['query'] }, searchContent);
|
|
291
|
+
reg('get_content_type_schema','Get the complete field schema for a specific content type, including field types, validation rules, and semantic mappings.', { type:'object', properties:{ type:{ type:'string' } }, required:['type'] }, getContentTypeSchema);
|
|
292
|
+
reg('count_content', 'Count the number of documents of a given type. Useful for statistics and pagination.', { type:'object', properties:{ type:{ type:'string' }, status:{ type:'string' } }, required:['type'] }, countContent);
|
|
293
|
+
reg('publish_content', 'Publish a draft document, making it publicly visible.', { type:'object', properties:{ type:{ type:'string' }, id:{ type:'string' } }, required:['type','id'] }, publishContent);
|
|
294
|
+
reg('archive_content', 'Archive a published document, removing it from public view.', { type:'object', properties:{ type:{ type:'string' }, id:{ type:'string' } }, required:['type','id'] }, archiveContent);
|
|
295
|
+
reg('batch_create_content', 'Create multiple documents of the same type in one operation.', { type:'object', properties:{ type:{ type:'string' }, items:{ type:'array', items:{ type:'object' } }, status:{ type:'string' } }, required:['type','items'] }, batchCreateContent);
|
|
296
|
+
reg('batch_update_content', 'Update multiple documents of the same type in one operation.', { type:'object', properties:{ type:{ type:'string' }, items:{ type:'array', items:{ type:'object', properties:{ id:{ type:'string' }, data:{ type:'object' } }, required:['id','data'] } } }, required:['type','items'] }, batchUpdateContent);
|
|
297
|
+
reg('clear_content', 'Delete ALL documents of a given type. Use with extreme caution.', { type:'object', properties:{ type:{ type:'string' } }, required:['type'] }, clearContent);
|
|
298
|
+
reg('list_media', 'List uploaded media files with URLs, sizes, and dimensions.', { type:'object', properties:{ limit:{ type:'number' } } }, listMedia);
|
|
299
|
+
reg('get_stats', 'Get system statistics: content type counts, total documents, server uptime.', { type:'object', properties:{} }, getStats);
|
|
300
|
+
reg('health_check', 'Check if the Taichu server is running and get its version, status, and uptime.', { type:'object', properties:{} }, healthCheck);
|
|
301
|
+
reg('get_content_by_field', 'Find documents where a specific field matches a given value. Useful for looking up content by slug, author, etc.', { type:'object', properties:{ type:{ type:'string' }, field:{ type:'string' }, value:{ type:'string' } }, required:['type','field','value'] }, getContentByField);
|
|
302
|
+
reg('export_content', 'Export all documents of a given type as structured JSON.', { type:'object', properties:{ type:{ type:'string' }, format:{ type:'string', enum:['json'] } }, required:['type'] }, exportContent);
|
|
303
|
+
reg('import_content', 'Bulk import documents from an array of data objects.', { type:'object', properties:{ type:{ type:'string' }, docs:{ type:'array', items:{ type:'object', properties:{ data:{ type:'object' }, status:{ type:'string' } }, required:['data'] } } }, required:['type','docs'] }, importContent);
|
|
304
|
+
reg('get_api_keys', 'List all API keys associated with your account.', { type:'object', properties:{} }, getApiKeys);
|
|
305
|
+
reg('create_api_key', 'Create a new API key for an AI agent to access Taichu. The raw key is only returned once.', { type:'object', properties:{ label:{ type:'string' } } }, createApiKey);
|
|
306
|
+
reg('rebuild_search_index', 'Rebuild the TF-IDF search index from all existing content. Use this after importing or migrating content.', { type:'object', properties:{} }, rebuildSearchIndex);
|
|
307
|
+
reg('get_content_relations', 'Discover content related to a document by analyzing field references and text similarity.', { type:'object', properties:{ type:{ type:'string' }, id:{ type:'string' } }, required:['type','id'] }, getContentRelations);
|
|
308
|
+
|
|
309
|
+
// ── v2.0 New Tools ────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
async function queryAuditLog(args) {
|
|
312
|
+
const params = new URLSearchParams();
|
|
313
|
+
if (args.actorId) params.set('actorId', args.actorId);
|
|
314
|
+
if (args.action) params.set('action', args.action);
|
|
315
|
+
if (args.limit) params.set('limit', String(args.limit));
|
|
316
|
+
const data = await request(`/audit?${params}`);
|
|
317
|
+
return ok({ total: data.total, entries: data.entries?.slice(0, 50).map(e => ({ action: e.action, actorType: e.actorType, resourceType: e.resourceType, createdAt: e.createdAt, summary: e.detail?.title || e.resourceId })) });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function getSiteSettings() {
|
|
321
|
+
const data = await request('/site-settings');
|
|
322
|
+
return ok(data);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function updateSiteSettings(args) {
|
|
326
|
+
const data = await request('/site-settings', { method: 'PUT', body: JSON.stringify(args) });
|
|
327
|
+
return ok(data);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function listPipelines() {
|
|
331
|
+
const data = await request('/pipelines');
|
|
332
|
+
return ok(data.templates || data);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
reg('query_audit_log', 'Query the audit log for content operations. Filter by actor, action type, or date range.', { type:'object', properties:{ actorId:{ type:'string' }, action:{ type:'string', enum:['create','update','delete','publish','archive','login','review'] }, limit:{ type:'number' } } }, queryAuditLog);
|
|
336
|
+
reg('get_site_settings', 'Get site configuration including ICP备案 number, analytics ID, site name, and language settings.', { type:'object', properties:{} }, getSiteSettings);
|
|
337
|
+
reg('update_site_settings', 'Update site configuration. Use this to set ICP备案号 (icpNumber), analytics, language, etc.', { type:'object', properties:{ icpNumber:{ type:'string' }, gonganNumber:{ type:'string' }, analyticsId:{ type:'string' }, siteName:{ type:'string' }, language:{ type:'string' } } }, updateSiteSettings);
|
|
338
|
+
reg('list_pipelines', 'List available content processing pipelines (translation, SEO, review). Use to discover Agent automation capabilities.', { type:'object', properties:{} }, listPipelines);
|
|
339
|
+
|
|
340
|
+
// ─────────────────────────────────────────────────────────────
|
|
341
|
+
// START
|
|
342
|
+
// ─────────────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
async function main() {
|
|
345
|
+
const transport = new StdioServerTransport();
|
|
346
|
+
console.error(`Taichu MCP Server v0.3.0`);
|
|
347
|
+
console.error(`API: ${API_BASE} | Auth: ${API_KEY ? 'API Key' : 'none'} | Tools: 24`);
|
|
348
|
+
console.error(`Ready for agent connections via stdio`);
|
|
349
|
+
await server.connect(transport);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
main().catch(err => {
|
|
353
|
+
console.error('MCP Server fatal error:', err.message);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
});
|