@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,104 @@
1
+ /**
2
+ * AuditLog — 操作审计日志
3
+ *
4
+ * 满足等保要求:操作日志 ≥ 6 个月留存,append-only。
5
+ * 记录人/Agent 的所有内容操作。
6
+ */
7
+
8
+ import { getStore } from './context.js';
9
+ import { createLogger } from './logger.js';
10
+
11
+ const log = createLogger('audit');
12
+
13
+ const RETENTION_DAYS = parseInt(process.env.TAICHU_AUDIT_RETENTION_DAYS) || 180;
14
+
15
+ /**
16
+ * @param {object} entry
17
+ * @param {string} entry.actorId — who (human id or agent key prefix)
18
+ * @param {string} entry.actorType — "human" | "agent"
19
+ * @param {string} entry.action — "create" | "update" | "delete" | "publish" | "archive" | "login" | "review"
20
+ * @param {string} entry.resourceType — content type name
21
+ * @param {string} entry.resourceId — document id
22
+ * @param {object} [entry.detail] — additional info
23
+ * @param {string} [entry.ip] — request IP
24
+ */
25
+ export async function record(entry) {
26
+ try {
27
+ const store = getStore();
28
+ await store.create({
29
+ type: 'audit_log',
30
+ data: {
31
+ actorId: entry.actorId || 'system',
32
+ actorType: entry.actorType || 'system',
33
+ action: entry.action,
34
+ resourceType: entry.resourceType || '',
35
+ resourceId: entry.resourceId || '',
36
+ detail: entry.detail || {},
37
+ ip: entry.ip || ''
38
+ },
39
+ status: 'active'
40
+ });
41
+ } catch (err) {
42
+ log.error(`Failed to record audit log: ${err.message}`);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Clean up logs older than retention period.
48
+ * Called periodically (daily).
49
+ */
50
+ export async function cleanupOldLogs() {
51
+ const store = getStore();
52
+ const cutoff = new Date(Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000).toISOString();
53
+
54
+ try {
55
+ const docs = await store.list({ type: 'audit_log', limit: 1000 });
56
+ let deleted = 0;
57
+ for (const doc of docs) {
58
+ if (doc.createdAt < cutoff) {
59
+ await store.delete(doc.id);
60
+ deleted++;
61
+ }
62
+ }
63
+ if (deleted > 0) log.info(`Audit log cleanup: removed ${deleted} entries older than ${RETENTION_DAYS} days`);
64
+ } catch (err) {
65
+ log.error(`Audit log cleanup failed: ${err.message}`);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Query audit logs.
71
+ * @param {object} filters — { actorId, action, resourceType, resourceId, limit, offset }
72
+ */
73
+ export async function query(filters = {}) {
74
+ const store = getStore();
75
+ const docs = await store.list({
76
+ type: 'audit_log',
77
+ limit: filters.limit || 50,
78
+ offset: filters.offset || 0,
79
+ orderBy: 'created_at',
80
+ order: 'desc'
81
+ });
82
+
83
+ return docs
84
+ .filter(d => {
85
+ if (filters.actorId && d.data.actorId !== filters.actorId) return false;
86
+ if (filters.action && d.data.action !== filters.action) return false;
87
+ if (filters.resourceType && d.data.resourceType !== filters.resourceType) return false;
88
+ return true;
89
+ })
90
+ .map(d => ({
91
+ id: d.id,
92
+ actorId: d.data.actorId,
93
+ actorType: d.data.actorType,
94
+ action: d.data.action,
95
+ resourceType: d.data.resourceType,
96
+ resourceId: d.data.resourceId,
97
+ detail: d.data.detail,
98
+ ip: d.data.ip,
99
+ createdAt: d.createdAt
100
+ }));
101
+ }
102
+
103
+ /** Run daily cleanup */
104
+ setInterval(cleanupOldLogs, 24 * 60 * 60 * 1000).unref();
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Auth Provider — 可插拔认证方式
3
+ *
4
+ * 内置:email/password(默认)、phone(短信验证码)、wechat(OAuth)
5
+ * 通过 @taichu/plugin-auth-providers 扩展。
6
+ *
7
+ * 使用:
8
+ * import { registerProvider, getProvider } from './auth-provider.js';
9
+ * registerProvider('phone', new PhoneAuthProvider({ smsService: 'aliyun' }));
10
+ */
11
+
12
+ class BaseAuthProvider {
13
+ constructor(name, config = {}) {
14
+ this.name = name;
15
+ this.config = config;
16
+ }
17
+
18
+ /** @returns {string} provider name */
19
+ getName() { return this.name; }
20
+
21
+ /** Validate credentials, return user id or null */
22
+ async validate(credentials) { throw new Error('Not implemented'); }
23
+
24
+ /** Get user display name */
25
+ async getUserInfo(userId) { throw new Error('Not implemented'); }
26
+ }
27
+
28
+ class EmailAuthProvider extends BaseAuthProvider {
29
+ constructor() { super('email'); }
30
+
31
+ async validate({ email, password }) {
32
+ // Delegated to auth.js (core)
33
+ return null; // Core auth handles email/password natively
34
+ }
35
+ }
36
+
37
+ class PhoneAuthProvider extends BaseAuthProvider {
38
+ constructor(config) { super('phone', config); }
39
+
40
+ async validate({ phone, code }) {
41
+ // Verify SMS code via configured service (aliyun/tencent)
42
+ // This is a stub — real implementation in plugin
43
+ if (!code || code.length < 4) return null;
44
+ return phone; // Return phone as identifier
45
+ }
46
+ }
47
+
48
+ class WechatAuthProvider extends BaseAuthProvider {
49
+ constructor(config) { super('wechat', config); }
50
+
51
+ async validate({ code }) {
52
+ // WeChat OAuth 2.0 flow
53
+ // Exchange code for access_token, then get user info
54
+ // This is a stub — real implementation in plugin
55
+ return code; // Return openid after exchange
56
+ }
57
+ }
58
+
59
+ // ── Registry ───────────────────────────────────────────────
60
+
61
+ const providers = new Map();
62
+ providers.set('email', new EmailAuthProvider());
63
+
64
+ export function registerProvider(name, provider) {
65
+ providers.set(name, provider);
66
+ }
67
+
68
+ export function getProvider(name) {
69
+ return providers.get(name);
70
+ }
71
+
72
+ export function listProviders() {
73
+ return Array.from(providers.keys());
74
+ }
75
+
76
+ export { EmailAuthProvider, PhoneAuthProvider, WechatAuthProvider, BaseAuthProvider };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Body Parser — 零依赖的请求体解析
3
+ *
4
+ * 支持:
5
+ * - application/json
6
+ * - multipart/form-data(未来,目前只有文件引用的需求)
7
+ */
8
+
9
+ export function parseBody(req) {
10
+ const MAX_BODY_SIZE = parseInt(process.env.TAICHU_MAX_BODY_SIZE) || 5 * 1024 * 1024; // default 5MB
11
+
12
+ return new Promise((resolve, reject) => {
13
+ const contentType = req.headers['content-type'] || '';
14
+
15
+ if (!contentType.includes('application/json')) {
16
+ resolve(null);
17
+ return;
18
+ }
19
+
20
+ let totalSize = 0;
21
+ const chunks = [];
22
+ req.on('data', chunk => {
23
+ totalSize += chunk.length;
24
+ if (totalSize > MAX_BODY_SIZE) {
25
+ reject(Object.assign(new Error('Request body too large'), {
26
+ code: 'PAYLOAD_TOO_LARGE',
27
+ status: 413,
28
+ maxSize: MAX_BODY_SIZE
29
+ }));
30
+ req.destroy();
31
+ return;
32
+ }
33
+ chunks.push(chunk);
34
+ });
35
+ req.on('end', () => {
36
+ const raw = Buffer.concat(chunks).toString('utf-8');
37
+ if (!raw.trim()) {
38
+ resolve(null);
39
+ return;
40
+ }
41
+ try {
42
+ resolve(JSON.parse(raw));
43
+ } catch (err) {
44
+ reject(Object.assign(new Error('Invalid JSON in request body'), {
45
+ code: 'INVALID_JSON',
46
+ status: 400
47
+ }));
48
+ }
49
+ });
50
+ req.on('error', reject);
51
+ });
52
+ }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Bootstrap — 初始化内置内容类型
3
+ *
4
+ * Taichu 启动时自动注册内置内容类型。
5
+ * 第三方扩展/插件可以通过 hook 系统注册自定义类型。
6
+ */
7
+
8
+ import { createContentType } from '../../core/src/content-type.js';
9
+ import { registerContentType } from './routes/api.js';
10
+
11
+ export function bootstrap() {
12
+ // Article — 博客文章
13
+ registerContentType(createContentType('article', {
14
+ label: '文章',
15
+ description: '博客文章类型,支持标题、正文、摘要、标签、分类等',
16
+ schemaOrg: 'Article',
17
+ fields: {
18
+ title: { type: 'string', required: true, maxLength: 200, semantic: 'headline' },
19
+ slug: { type: 'string', required: true, semantic: 'identifier' },
20
+ body: { type: 'json', required: true, semantic: 'articleBody' },
21
+ excerpt: { type: 'string', maxLength: 500, semantic: 'description' },
22
+ featuredImage: { type: 'media', semantic: 'image' },
23
+ tags: { type: 'array', items: { type: 'string' }, semantic: 'keywords' },
24
+ category: { type: 'relation', target: 'category' },
25
+ status: { type: 'enum', values: ['draft', 'scheduled', 'published', 'archived'] },
26
+ publishedAt: { type: 'datetime' }
27
+ }
28
+ }));
29
+
30
+ // Page — 静态页面
31
+ registerContentType(createContentType('page', {
32
+ label: '页面',
33
+ description: '静态页面,如关于、联系、隐私政策',
34
+ schemaOrg: 'WebPage',
35
+ fields: {
36
+ title: { type: 'string', required: true, maxLength: 200 },
37
+ slug: { type: 'string', required: true },
38
+ body: { type: 'json', required: true },
39
+ status: { type: 'enum', values: ['draft', 'scheduled', 'published', 'archived'] },
40
+ order: { type: 'number' }
41
+ }
42
+ }));
43
+
44
+ // Category — 分类
45
+ registerContentType(createContentType('category', {
46
+ label: '分类',
47
+ description: '内容分类(关联文章等)',
48
+ fields: {
49
+ name: { type: 'string', required: true, maxLength: 100 },
50
+ slug: { type: 'string', required: true },
51
+ description: { type: 'string', maxLength: 500 },
52
+ parent: { type: 'relation', target: 'category' }
53
+ }
54
+ }));
55
+
56
+ // Media — 媒体文件元数据
57
+ registerContentType(createContentType('media', {
58
+ label: '媒体',
59
+ description: '上传文件的元数据记录',
60
+ schemaOrg: 'MediaObject',
61
+ fields: {
62
+ filename: { type: 'string', required: true },
63
+ mimeType: { type: 'string', required: true },
64
+ size: { type: 'number' },
65
+ url: { type: 'string', required: true },
66
+ width: { type: 'number' },
67
+ height: { type: 'number' },
68
+ altText: { type: 'string' },
69
+ caption: { type: 'string' },
70
+ uploadedBy: { type: 'string' }
71
+ }
72
+ }));
73
+
74
+ // Author — 作者/贡献者(未来支持 Agent 作者)
75
+ registerContentType(createContentType('author', {
76
+ label: '作者',
77
+ description: '内容作者——可以是人类也可以是 AI Agent',
78
+ schemaOrg: 'Person',
79
+ fields: {
80
+ name: { type: 'string', required: true },
81
+ slug: { type: 'string', required: true },
82
+ bio: { type: 'string', maxLength: 1000 },
83
+ avatar: { type: 'media' },
84
+ website: { type: 'string' },
85
+ email: { type: 'string' },
86
+ type: { type: 'enum', values: ['human', 'agent'], default: 'human' }
87
+ }
88
+ }));
89
+
90
+ // User — 系统用户(人类管理员/作者)
91
+ registerContentType(createContentType('user', {
92
+ label: '用户',
93
+ description: '系统用户——人类作者、编辑、管理员',
94
+ fields: {
95
+ username: { type: 'string', required: true, maxLength: 50 },
96
+ email: { type: 'string' },
97
+ password: { type: 'string', required: true }, // 只存哈希
98
+ role: { type: 'enum', values: ['admin', 'editor', 'author'], default: 'author' }
99
+ }
100
+ }));
101
+
102
+ // API Key — Agent 认证密钥
103
+ registerContentType(createContentType('api_key', {
104
+ label: 'API Key',
105
+ description: 'AI Agent 认证密钥(用于 Agent 访问 API)',
106
+ fields: {
107
+ prefix: { type: 'string', required: true },
108
+ hash: { type: 'string', required: true },
109
+ label: { type: 'string' },
110
+ ownerId: { type: 'string', required: true },
111
+ scopes: { type: 'array', items: { type: 'string' } }
112
+ }
113
+ }));
114
+
115
+ // Webhook — 内容变更事件推送
116
+ registerContentType(createContentType('webhook', {
117
+ label: 'Webhook',
118
+ description: '内容变更事件的外部推送端点',
119
+ fields: {
120
+ url: { type: 'string', required: true },
121
+ events: { type: 'array', items: { type: 'string' } },
122
+ types: { type: 'array', items: { type: 'string' } },
123
+ secret: { type: 'string', required: true },
124
+ label: { type: 'string' },
125
+ active: { type: 'boolean' },
126
+ stats: { type: 'json' }
127
+ }
128
+ }));
129
+
130
+ // AuditLog — 操作审计日志(append-only,≥180天)
131
+ registerContentType(createContentType('audit_log', {
132
+ label: '审计日志',
133
+ description: '操作审计日志,满足等保合规要求,保留≥180天',
134
+ fields: {
135
+ actorId: { type: 'string', required: true },
136
+ actorType: { type: 'string', required: true },
137
+ action: { type: 'string', required: true },
138
+ resourceType: { type: 'string' },
139
+ resourceId: { type: 'string' },
140
+ detail: { type: 'json' },
141
+ ip: { type: 'string' }
142
+ }
143
+ }));
144
+
145
+ // SiteSettings — 站点全局配置
146
+ registerContentType(createContentType('site_settings', {
147
+ label: '站点配置',
148
+ description: '全局站点配置:站点信息、Hero区块、作者、备案等',
149
+ fields: {
150
+ siteName: { type: 'string' },
151
+ siteDescription: { type: 'string' },
152
+ icpNumber: { type: 'string' },
153
+ gonganNumber: { type: 'string' },
154
+ analyticsId: { type: 'string' },
155
+ language: { type: 'string' },
156
+ timezone: { type: 'string' },
157
+ seoTitle: { type: 'string' },
158
+ seoDescription: { type: 'string' },
159
+ seoKeywords: { type: 'array', items: { type: 'string' } },
160
+ authorName: { type: 'string' },
161
+ authorTitle: { type: 'string' },
162
+ authorBio: { type: 'string' },
163
+ authorAvatar: { type: 'media' },
164
+ hero_style: { type: 'string' },
165
+ hero_video_url: { type: 'string' },
166
+ hero_video_poster: { type: 'string' },
167
+ hero_image_url: { type: 'string' },
168
+ hero_headline: { type: 'string' },
169
+ hero_subtitle: { type: 'string' },
170
+ hero_cta_text: { type: 'string' },
171
+ hero_cta_link: { type: 'string' },
172
+ hero_overlay_opacity: { type: 'number' },
173
+ theme: { type: 'json' }
174
+ }
175
+ }));
176
+
177
+ // ReviewPolicy — Agent 内容审核策略
178
+ registerContentType(createContentType('review_policy', {
179
+ label: '审核策略',
180
+ description: 'Agent 生成内容的自动审核策略配置',
181
+ fields: {
182
+ name: { type: 'string', required: true },
183
+ rules: { type: 'json' },
184
+ requireHuman: { type: 'boolean' },
185
+ blockedTerms: { type: 'array', items: { type: 'string' } },
186
+ active: { type: 'boolean' }
187
+ }
188
+ }));
189
+
190
+ // Revision — 内容版本快照
191
+ registerContentType(createContentType('revision', {
192
+ label: '版本历史',
193
+ description: '文档的版本快照,用于版本管理和回滚',
194
+ fields: {
195
+ docId: { type: 'string', required: true },
196
+ docType: { type: 'string' },
197
+ data: { type: 'json', required: true },
198
+ status: { type: 'string' },
199
+ meta: { type: 'json' },
200
+ author: { type: 'string' },
201
+ authorType: { type: 'string' },
202
+ timestamp: { type: 'string' }
203
+ }
204
+ }));
205
+
206
+ // Navigation — 站点导航菜单
207
+ registerContentType(createContentType('navigation', {
208
+ label: '导航菜单',
209
+ description: '站点前台导航菜单项',
210
+ fields: {
211
+ title: { type: 'string', required: true },
212
+ url: { type: 'string', required: true },
213
+ order: { type: 'number' },
214
+ target: { type: 'string' }
215
+ }
216
+ }));
217
+
218
+ // Comment — 文章评论(支持嵌套回复)
219
+ registerContentType(createContentType('comment', {
220
+ label: '评论',
221
+ description: '文章评论,支持审核和嵌套回复',
222
+ schemaOrg: 'Comment',
223
+ fields: {
224
+ postId: { type: 'relation', target: 'article', required: true },
225
+ author: { type: 'string', required: true, maxLength: 100 },
226
+ email: { type: 'string' },
227
+ body: { type: 'string', required: true, maxLength: 5000 },
228
+ status: { type: 'enum', values: ['pending', 'approved', 'spam'], default: 'pending' },
229
+ parentId: { type: 'relation', target: 'comment' }
230
+ }
231
+ }));
232
+
233
+ // App — 应用/工具展示
234
+ registerContentType(createContentType('app', {
235
+ label: '应用',
236
+ description: '应用或工具展示(Market页面)',
237
+ schemaOrg: 'SoftwareApplication',
238
+ fields: {
239
+ name: { type: 'string', required: true, maxLength: 100 },
240
+ slug: { type: 'string', required: true },
241
+ description: { type: 'string', maxLength: 1000 },
242
+ icon: { type: 'media' },
243
+ cover: { type: 'media' },
244
+ category: { type: 'string' },
245
+ url: { type: 'string' },
246
+ status: { type: 'enum', values: ['live', 'dev', 'archived'], default: 'live' },
247
+ featured: { type: 'boolean', default: false },
248
+ order: { type: 'number' }
249
+ }
250
+ }));
251
+
252
+ // Agent — AI Agent/Skill 展示
253
+ registerContentType(createContentType('agent', {
254
+ label: 'Agent',
255
+ description: 'AI Agent 或 Skill 展示',
256
+ fields: {
257
+ name: { type: 'string', required: true, maxLength: 100 },
258
+ slug: { type: 'string', required: true },
259
+ description: { type: 'string', maxLength: 1000 },
260
+ icon: { type: 'media' },
261
+ cover: { type: 'media' },
262
+ category: { type: 'string' },
263
+ type: { type: 'string' },
264
+ package_url: { type: 'string' },
265
+ status: { type: 'enum', values: ['live', 'dev', 'archived'], default: 'live' },
266
+ featured: { type: 'boolean', default: false },
267
+ order: { type: 'number' }
268
+ }
269
+ }));
270
+
271
+ console.log(` Bootstrap: 16 content types registered`);
272
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Collaboration Engine — 多 Agent 协作控制
3
+ *
4
+ * 提供乐观锁(Optimistic Locking)和内容冲突检测。
5
+ *
6
+ * 乐观锁机制:
7
+ * - 每个文档有一个 `_version` 字段(单调递增整数)
8
+ * - 更新时必须传入 `_version`,服务端比对
9
+ * - 版本不匹配 → 409 Conflict,返回当前版本
10
+ * - 客户端拿到最新版本后可重试
11
+ *
12
+ * 协作会话:
13
+ * - Agent 可以声明"I'm editing this"(acquire session)
14
+ * - 会话有过期时间(默认 5 分钟)
15
+ * - 其他 Agent 可以看到当前编辑者
16
+ */
17
+
18
+ const SESSION_TTL = 5 * 60 * 1000; // 5 minutes
19
+
20
+ class CollaborationEngine {
21
+ constructor() {
22
+ /** @type {Map<string, { actorId: string, actorType: string, label: string, startedAt: number, expiresAt: number }>} */
23
+ this.sessions = new Map();
24
+ this._cleanupTimer = setInterval(() => this._cleanup(), 60000);
25
+ }
26
+
27
+ /**
28
+ * Acquire an editing session for a document.
29
+ * @param {string} docId
30
+ * @param {object} actor — { id, type, username/label }
31
+ * @returns {{ acquired: boolean, currentEditor?: object, message?: string }}
32
+ */
33
+ acquire(docId, actor) {
34
+ const existing = this.sessions.get(docId);
35
+
36
+ if (existing && existing.expiresAt > Date.now()) {
37
+ // Still active — someone is editing
38
+ if (existing.actorId === actor.id) {
39
+ // Same actor refreshing their session
40
+ existing.expiresAt = Date.now() + SESSION_TTL;
41
+ return { acquired: true };
42
+ }
43
+ return {
44
+ acquired: false,
45
+ currentEditor: {
46
+ id: existing.actorId,
47
+ type: existing.actorType,
48
+ label: existing.label
49
+ },
50
+ message: `Document is being edited by ${existing.label || existing.actorId}`
51
+ };
52
+ }
53
+
54
+ this.sessions.set(docId, {
55
+ actorId: actor.id,
56
+ actorType: actor.type || 'agent',
57
+ label: actor.username || actor.label || actor.id,
58
+ startedAt: Date.now(),
59
+ expiresAt: Date.now() + SESSION_TTL
60
+ });
61
+
62
+ return { acquired: true };
63
+ }
64
+
65
+ /**
66
+ * Release an editing session.
67
+ */
68
+ release(docId, actorId) {
69
+ const existing = this.sessions.get(docId);
70
+ if (existing && existing.actorId === actorId) {
71
+ this.sessions.delete(docId);
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /**
78
+ * Check if a document has an active editing session.
79
+ */
80
+ getSession(docId) {
81
+ const existing = this.sessions.get(docId);
82
+ if (existing && existing.expiresAt > Date.now()) {
83
+ return {
84
+ editorId: existing.actorId,
85
+ editorType: existing.actorType,
86
+ label: existing.label,
87
+ startedAt: new Date(existing.startedAt).toISOString()
88
+ };
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /**
94
+ * Optimistic lock check for document updates.
95
+ * @param {object} doc — current document from store
96
+ * @param {number} expectedVersion — version from client
97
+ * @returns {{ ok: boolean, currentVersion?: number, error?: string }}
98
+ */
99
+ checkVersion(doc, expectedVersion) {
100
+ const currentVersion = doc._version || doc.meta?.version || 0;
101
+
102
+ if (expectedVersion !== undefined && expectedVersion !== currentVersion) {
103
+ return {
104
+ ok: false,
105
+ currentVersion,
106
+ error: `Version conflict: expected v${expectedVersion}, current v${currentVersion}. Re-fetch and retry.`
107
+ };
108
+ }
109
+
110
+ return { ok: true };
111
+ }
112
+
113
+ /**
114
+ * List all active sessions.
115
+ */
116
+ listSessions() {
117
+ const active = [];
118
+ for (const [docId, session] of this.sessions) {
119
+ if (session.expiresAt > Date.now()) {
120
+ active.push({
121
+ docId,
122
+ editorId: session.actorId,
123
+ editorType: session.actorType,
124
+ label: session.label,
125
+ remainingSeconds: Math.floor((session.expiresAt - Date.now()) / 1000)
126
+ });
127
+ }
128
+ }
129
+ return active;
130
+ }
131
+
132
+ _cleanup() {
133
+ const now = Date.now();
134
+ for (const [docId, session] of this.sessions) {
135
+ if (session.expiresAt <= now) {
136
+ this.sessions.delete(docId);
137
+ }
138
+ }
139
+ }
140
+
141
+ destroy() {
142
+ if (this._cleanupTimer) clearInterval(this._cleanupTimer);
143
+ this.sessions.clear();
144
+ }
145
+ }
146
+
147
+ // ── Singleton ──────────────────────────────────────────────
148
+
149
+ let _collab = null;
150
+
151
+ export function getCollab() {
152
+ if (!_collab) _collab = new CollaborationEngine();
153
+ return _collab;
154
+ }