@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.
Files changed (93) hide show
  1. package/.dockerignore +13 -0
  2. package/Dockerfile +51 -0
  3. package/LICENSE +21 -0
  4. package/README.md +208 -0
  5. package/docker-compose.yml +42 -0
  6. package/docs/ROADMAP.md +101 -0
  7. package/docs/api/README.md +102 -0
  8. package/docs/architecture/001-zero-dependency-core.md +61 -0
  9. package/docs/architecture/002-structured-content-model.md +70 -0
  10. package/docs/architecture/003-hook-based-extension.md +82 -0
  11. package/docs/architecture/004-api-first-architecture.md +122 -0
  12. package/docs/architecture/README.md +24 -0
  13. package/docs/logo.svg +40 -0
  14. package/docs/research/ai-era-cms-user-research.md +247 -0
  15. package/docs/zh/README.md +81 -0
  16. package/docs/zh/guides/deploy.md +75 -0
  17. package/docs/zh/guides/mcp.md +84 -0
  18. package/docs/zh/guides/promotion.md +51 -0
  19. package/marketplace.json +78 -0
  20. package/package.json +60 -0
  21. package/packages/core/src/auth.js +158 -0
  22. package/packages/core/src/content-type.js +244 -0
  23. package/packages/core/src/core.test.js +406 -0
  24. package/packages/core/src/errors.js +60 -0
  25. package/packages/core/src/hooks.js +104 -0
  26. package/packages/core/src/index.js +16 -0
  27. package/packages/core/src/server.test.js +149 -0
  28. package/packages/core/src/sm-crypto.js +31 -0
  29. package/packages/core/src/sqlite-store.js +354 -0
  30. package/packages/core/src/store.js +174 -0
  31. package/packages/core/src/tokenizer.js +89 -0
  32. package/packages/core/src/vector-index.js +131 -0
  33. package/packages/llm-providers/src/index.js +181 -0
  34. package/packages/mcp/src/index.js +355 -0
  35. package/packages/server/public/admin/assets/index-DApxOVTx.js +191 -0
  36. package/packages/server/public/admin/assets/index-DtMvdQm9.css +1 -0
  37. package/packages/server/public/admin/index.html +28 -0
  38. package/packages/server/public/aurora/style.css +1173 -0
  39. package/packages/server/public/favicon.svg +46 -0
  40. package/packages/server/public/theme/index.html +288 -0
  41. package/packages/server/public/theme/style.css +133 -0
  42. package/packages/server/public/theme-minimal/index.html +223 -0
  43. package/packages/server/public/theme-minimal/style.css +109 -0
  44. package/packages/server/public/ws-test.html +106 -0
  45. package/packages/server/src/activitypub.js +228 -0
  46. package/packages/server/src/audit.js +104 -0
  47. package/packages/server/src/auth-provider.js +76 -0
  48. package/packages/server/src/body-parser.js +52 -0
  49. package/packages/server/src/bootstrap.js +272 -0
  50. package/packages/server/src/collab.js +154 -0
  51. package/packages/server/src/config.js +136 -0
  52. package/packages/server/src/context.js +86 -0
  53. package/packages/server/src/email.js +317 -0
  54. package/packages/server/src/index.js +195 -0
  55. package/packages/server/src/logger.js +78 -0
  56. package/packages/server/src/media-store.js +213 -0
  57. package/packages/server/src/middleware/auth.js +203 -0
  58. package/packages/server/src/middleware/cors.js +15 -0
  59. package/packages/server/src/middleware/error-handler.js +49 -0
  60. package/packages/server/src/middleware/rate-limit.js +118 -0
  61. package/packages/server/src/multipart.js +150 -0
  62. package/packages/server/src/notify.js +126 -0
  63. package/packages/server/src/pipeline.js +206 -0
  64. package/packages/server/src/plugin-installer.js +139 -0
  65. package/packages/server/src/plugin-manager.js +165 -0
  66. package/packages/server/src/relationships.js +217 -0
  67. package/packages/server/src/revisions.js +114 -0
  68. package/packages/server/src/router.js +194 -0
  69. package/packages/server/src/routes/activitypub.js +140 -0
  70. package/packages/server/src/routes/api.js +363 -0
  71. package/packages/server/src/routes/audit.js +222 -0
  72. package/packages/server/src/routes/auth.js +205 -0
  73. package/packages/server/src/routes/collab.js +90 -0
  74. package/packages/server/src/routes/export.js +77 -0
  75. package/packages/server/src/routes/graphql.js +344 -0
  76. package/packages/server/src/routes/media.js +169 -0
  77. package/packages/server/src/routes/plugin-marketplace.js +171 -0
  78. package/packages/server/src/routes/relationships.js +133 -0
  79. package/packages/server/src/routes/rss.js +92 -0
  80. package/packages/server/src/routes/sso.js +211 -0
  81. package/packages/server/src/routes/theme.js +119 -0
  82. package/packages/server/src/routes/webhook.js +94 -0
  83. package/packages/server/src/routes/wechat.js +115 -0
  84. package/packages/server/src/routes/workflow.js +157 -0
  85. package/packages/server/src/scheduler.js +96 -0
  86. package/packages/server/src/search.js +100 -0
  87. package/packages/server/src/server.test.js +295 -0
  88. package/packages/server/src/sso-analytics.js +78 -0
  89. package/packages/server/src/static.js +70 -0
  90. package/packages/server/src/theme-engine.js +119 -0
  91. package/packages/server/src/webhook.js +192 -0
  92. package/packages/server/src/websocket.js +308 -0
  93. package/scripts/cli.js +90 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Taichu Errors — 结构化错误类型
3
+ *
4
+ * 所有 Taichu 内部错误都使用这些类,方便:
5
+ * - 前端精确展示错误信息
6
+ * - Agent 通过错误码做决策
7
+ * - 日志系统分类处理
8
+ */
9
+
10
+ export class TaichuError extends Error {
11
+ constructor(message, code = 'TAICHU_ERROR', status = 500) {
12
+ super(message);
13
+ this.name = 'TaichuError';
14
+ this.code = code;
15
+ this.status = status;
16
+ }
17
+
18
+ toJSON() {
19
+ return {
20
+ error: this.code,
21
+ message: this.message,
22
+ status: this.status
23
+ };
24
+ }
25
+ }
26
+
27
+ export class ValidationError extends TaichuError {
28
+ constructor(message) {
29
+ super(message, 'VALIDATION_ERROR', 400);
30
+ this.name = 'ValidationError';
31
+ }
32
+ }
33
+
34
+ export class NotFoundError extends TaichuError {
35
+ constructor(resource = 'Resource') {
36
+ super(`${resource} not found`, 'NOT_FOUND', 404);
37
+ this.name = 'NotFoundError';
38
+ }
39
+ }
40
+
41
+ export class UnauthorizedError extends TaichuError {
42
+ constructor(message = 'Unauthorized') {
43
+ super(message, 'UNAUTHORIZED', 401);
44
+ this.name = 'UnauthorizedError';
45
+ }
46
+ }
47
+
48
+ export class ForbiddenError extends TaichuError {
49
+ constructor(message = 'Forbidden') {
50
+ super(message, 'FORBIDDEN', 403);
51
+ this.name = 'ForbiddenError';
52
+ }
53
+ }
54
+
55
+ export class ConflictError extends TaichuError {
56
+ constructor(message) {
57
+ super(message, 'CONFLICT', 409);
58
+ this.name = 'ConflictError';
59
+ }
60
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Hook System — 插件/扩展的生命周期钩子
3
+ *
4
+ * Taichu 的插件系统基于生命周期钩子(Lifecycle Hooks)。
5
+ * 与 WordPress 的 add_action / add_filter 精神一致,但:
6
+ * - 纯函数式,无全局状态
7
+ * - 支持异步钩子
8
+ * - 支持优先级排序
9
+ * - 钩子返回值可传递(类似 filter)或纯副作用(类似 action)
10
+ *
11
+ * 内置钩子:
12
+ * - beforeCreate / afterCreate
13
+ * - beforeUpdate / afterUpdate
14
+ * - beforeDelete / afterDelete
15
+ * - beforePublish / afterPublish
16
+ * - beforeRender / afterRender
17
+ * - agent:onRequest — Agent 请求拦截
18
+ * - agent:onResponse — Agent 响应拦截
19
+ */
20
+
21
+ export function createHookSystem() {
22
+ /** @type {Map<string, Array<{id: string, fn: Function, priority: number}>>} */
23
+ const hooks = new Map();
24
+
25
+ /**
26
+ * Register a hook handler.
27
+ * @param {string} name — hook name
28
+ * @param {Function} fn — handler function
29
+ * @param {number} [priority=10] — lower runs first (like WordPress)
30
+ * @returns {Function} — deregister function
31
+ */
32
+ function on(name, fn, priority = 10) {
33
+ if (!hooks.has(name)) {
34
+ hooks.set(name, []);
35
+ }
36
+ const id = `${name}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
37
+ const entry = { id, fn, priority };
38
+ hooks.get(name).push(entry);
39
+
40
+ // Sort by priority (ascending)
41
+ hooks.get(name).sort((a, b) => a.priority - b.priority);
42
+
43
+ // Return deregister function
44
+ return () => {
45
+ const list = hooks.get(name);
46
+ if (list) {
47
+ const idx = list.findIndex(e => e.id === id);
48
+ if (idx !== -1) list.splice(idx, 1);
49
+ }
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Run all handlers for a hook.
55
+ *
56
+ * @param {string} name — hook name
57
+ * @param {*} payload — initial payload (passed through handlers)
58
+ * @param {object} [context] — extra context (store, agent identity, etc.)
59
+ * @returns {Promise<*>} — final payload after all handlers
60
+ */
61
+ async function run(name, payload, context = {}) {
62
+ const handlers = hooks.get(name);
63
+ if (!handlers || handlers.length === 0) return payload;
64
+
65
+ let result = payload;
66
+ for (const entry of handlers) {
67
+ try {
68
+ const returned = await entry.fn(result, context);
69
+ // If a handler returns null explicitly, stop the chain
70
+ if (returned === null) {
71
+ context._stoppedAt = name;
72
+ break;
73
+ }
74
+ // Only update if handler returned a value (allows pass-through for side-effect handlers)
75
+ if (returned !== undefined) {
76
+ result = returned;
77
+ }
78
+ } catch (err) {
79
+ // Re-throw with hook context
80
+ throw new Error(`Hook "${name}" handler error: ${err.message}`, { cause: err });
81
+ }
82
+ }
83
+ return result;
84
+ }
85
+
86
+ /**
87
+ * List registered hook names.
88
+ * @returns {string[]}
89
+ */
90
+ function list() {
91
+ return Array.from(hooks.keys());
92
+ }
93
+
94
+ /**
95
+ * Get handler count for a hook.
96
+ * @param {string} name
97
+ * @returns {number}
98
+ */
99
+ function count(name) {
100
+ return (hooks.get(name) || []).length;
101
+ }
102
+
103
+ return Object.freeze({ on, run, list, count });
104
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @taichu/core — Content Model & Storage Abstraction
3
+ *
4
+ * Taichu 的核心哲学:
5
+ * - 一切内容都是"文档"(Document),有类型(ContentType)和语义标记
6
+ * - 存储是抽象的——今天用 SQLite,明天可以换 Postgres 或文件系统
7
+ * - 内容不存 HTML 字符串,存结构化数据,由渲染层负责输出格式
8
+ * - Agent 和人类共享同一套内容模型,只是权限不同
9
+ */
10
+
11
+ export { createContentType } from './content-type.js';
12
+ export { createStore, createMemoryStore } from './store.js';
13
+ export { createSQLiteStore } from './sqlite-store.js';
14
+ export { createHookSystem } from './hooks.js';
15
+ export { TaichuError, ValidationError, NotFoundError, UnauthorizedError, ForbiddenError, ConflictError } from './errors.js';
16
+ export { hashPassword, verifyPassword, signJWT, verifyJWT, generateAPIKey, verifyAPIKey } from './auth.js';
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Server module tests — rate limiter, revisions, audit, pipeline
3
+ */
4
+ import { describe, it, before, after } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+
7
+ // ════════════════════════════════════════════════════════════
8
+ // Rate Limiter
9
+ // ════════════════════════════════════════════════════════════
10
+
11
+ describe('RateLimiter', () => {
12
+ it('should allow requests within limit', async () => {
13
+ const { rateLimit } = await import('../../server/src/middleware/rate-limit.js');
14
+ // Create a mock ctx
15
+ const ctx = { req: { headers: {}, socket: { remoteAddress: '127.0.0.1' } }, res: { writeHead() {}, end() {} }, url: new URL('http://localhost/test') };
16
+ // First requests should pass
17
+ for (let i = 0; i < 5; i++) {
18
+ assert.equal(rateLimit(ctx), true);
19
+ }
20
+ });
21
+
22
+ it('should track by IP', async () => {
23
+ const { rateLimit } = await import('../../server/src/middleware/rate-limit.js');
24
+ const ctx = { req: { headers: {}, socket: { remoteAddress: '10.0.0.1' } }, res: { writeHead() {}, end() {} }, url: new URL('http://localhost/test') };
25
+ assert.equal(rateLimit(ctx), true);
26
+ });
27
+ });
28
+
29
+ // ════════════════════════════════════════════════════════════
30
+ // Revisions & Diff
31
+ // ════════════════════════════════════════════════════════════
32
+
33
+ describe('Revisions', () => {
34
+ it('should diff two objects', async () => {
35
+ const { diffObjects } = await import('../../server/src/revisions.js');
36
+ const changes = diffObjects(
37
+ { title: 'Old', body: 'same' },
38
+ { title: 'New', body: 'same' }
39
+ );
40
+ assert.equal(changes.length, 1);
41
+ assert.equal(changes[0].field, 'title');
42
+ assert.equal(changes[0].from, 'Old');
43
+ assert.equal(changes[0].to, 'New');
44
+ });
45
+
46
+ it('should detect added fields', async () => {
47
+ const { diffObjects } = await import('../../server/src/revisions.js');
48
+ const changes = diffObjects(
49
+ { title: 'Same' },
50
+ { title: 'Same', body: 'new field' }
51
+ );
52
+ assert.equal(changes.length, 1);
53
+ assert.equal(changes[0].field, 'body');
54
+ });
55
+
56
+ it('should return empty for identical objects', async () => {
57
+ const { diffObjects } = await import('../../server/src/revisions.js');
58
+ const changes = diffObjects({ a: 1, b: [2] }, { a: 1, b: [2] });
59
+ assert.equal(changes.length, 0);
60
+ });
61
+ });
62
+
63
+ // ════════════════════════════════════════════════════════════
64
+ // Pipeline Engine
65
+ // ════════════════════════════════════════════════════════════
66
+
67
+ describe('PipelineEngine', () => {
68
+ it('should list built-in templates', async () => {
69
+ const { TEMPLATES } = await import('../../server/src/pipeline.js');
70
+ assert.ok(TEMPLATES.translation);
71
+ assert.ok(TEMPLATES.seo);
72
+ assert.ok(TEMPLATES.review);
73
+ assert.equal(Object.keys(TEMPLATES).length, 3);
74
+ });
75
+
76
+ it('should have correct step counts', async () => {
77
+ const { TEMPLATES } = await import('../../server/src/pipeline.js');
78
+ assert.equal(TEMPLATES.translation.steps.length, 4);
79
+ assert.equal(TEMPLATES.seo.steps.length, 5);
80
+ assert.equal(TEMPLATES.review.steps.length, 2);
81
+ });
82
+ });
83
+
84
+ // ════════════════════════════════════════════════════════════
85
+ // Review Policy
86
+ // ════════════════════════════════════════════════════════════
87
+
88
+ describe('ReviewPolicy', () => {
89
+ it('should approve non-agent content', async () => {
90
+ const { ReviewPolicy } = await import('../../server/src/pipeline.js');
91
+ const policy = new ReviewPolicy({ requireHumanReview: true });
92
+ const result = policy.evaluate({ data: { title: 'test' } });
93
+ assert.equal(result.approved, true);
94
+ });
95
+
96
+ it('should block agent content when requireHumanReview is true', async () => {
97
+ const { ReviewPolicy } = await import('../../server/src/pipeline.js');
98
+ const policy = new ReviewPolicy({ requireHumanReview: true });
99
+ const result = policy.evaluate({ data: { title: 'test' }, _meta: { createdBy: { type: 'agent' } } });
100
+ assert.equal(result.approved, false);
101
+ });
102
+
103
+ it('should allow agent content without review requirement', async () => {
104
+ const { ReviewPolicy } = await import('../../server/src/pipeline.js');
105
+ const policy = new ReviewPolicy({ requireHumanReview: false });
106
+ const result = policy.evaluate({ data: {}, _meta: { createdBy: { type: 'agent' } } });
107
+ assert.equal(result.approved, true);
108
+ });
109
+ });
110
+
111
+ // ════════════════════════════════════════════════════════════
112
+ // LLM Providers
113
+ // ════════════════════════════════════════════════════════════
114
+
115
+ describe('LLMProviders', () => {
116
+ it('should list all providers', async () => {
117
+ const { listProviders, createProvider } = await import('../../llm-providers/src/index.js');
118
+ const providers = listProviders();
119
+ assert.ok(providers.includes('qwen'));
120
+ assert.ok(providers.includes('deepseek'));
121
+ assert.ok(providers.includes('ernie'));
122
+ assert.ok(providers.includes('moonshot'));
123
+ });
124
+
125
+ it('should create a provider instance', async () => {
126
+ const { createProvider } = await import('../../llm-providers/src/index.js');
127
+ const provider = createProvider('deepseek', { apiKey: 'test-key' });
128
+ assert.equal(provider.defaultModel, 'deepseek-chat');
129
+ assert.ok(provider.baseURL.includes('deepseek.com'));
130
+ });
131
+ });
132
+
133
+ // ════════════════════════════════════════════════════════════
134
+ // Auth Provider
135
+ // ════════════════════════════════════════════════════════════
136
+
137
+ describe('AuthProviders', () => {
138
+ it('should list default providers', async () => {
139
+ const { listProviders } = await import('../../server/src/auth-provider.js');
140
+ const providers = listProviders();
141
+ assert.ok(providers.includes('email'));
142
+ });
143
+
144
+ it('should register a custom provider', async () => {
145
+ const { registerProvider, getProvider } = await import('../../server/src/auth-provider.js');
146
+ registerProvider('test-provider', { getName: () => 'test-provider' });
147
+ assert.ok(getProvider('test-provider'));
148
+ });
149
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * SM Crypto — 国密算法适配(P2-02)
3
+ *
4
+ * 生产环境使用 @taichu/plugin-sm-crypto 独立包。
5
+ * 当前回退到 Node.js 原生算法。
6
+ */
7
+
8
+ import { createHash, createCipheriv, randomBytes } from 'node:crypto';
9
+
10
+ export function sm3Hash(data) {
11
+ const enableSM = process.env.TAICHU_SM_CRYPTO === '1';
12
+ if (enableSM) {
13
+ // Plugin provides real SM3
14
+ }
15
+ return createHash('sha256').update(data).digest('hex');
16
+ }
17
+
18
+ export function sm4Encrypt(plaintext) {
19
+ if (process.env.TAICHU_SM_CRYPTO === '1') {
20
+ throw new Error('SM4 requires @taichu/plugin-sm-crypto.');
21
+ }
22
+ return { algorithm: 'aes-256-gcm', data: plaintext };
23
+ }
24
+
25
+ export function sm2Sign() {
26
+ throw new Error('SM2 requires @taichu/plugin-sm-crypto.');
27
+ }
28
+
29
+ export function isSMEnabled() {
30
+ return process.env.TAICHU_SM_CRYPTO === '1';
31
+ }