@cc-soul/openclaw 1.0.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/README.md +284 -0
- package/cc-soul/HOOK.md +18 -0
- package/cc-soul/body.js +129 -0
- package/cc-soul/cli.js +263 -0
- package/cc-soul/cognition.js +143 -0
- package/cc-soul/context-prep.js +1 -0
- package/cc-soul/epistemic.js +1 -0
- package/cc-soul/evolution.js +176 -0
- package/cc-soul/features.js +79 -0
- package/cc-soul/federation.js +207 -0
- package/cc-soul/fingerprint.js +1 -0
- package/cc-soul/flow.js +199 -0
- package/cc-soul/graph.js +85 -0
- package/cc-soul/handler.js +609 -0
- package/cc-soul/inner-life.js +1 -0
- package/cc-soul/lorebook.js +94 -0
- package/cc-soul/memory.js +688 -0
- package/cc-soul/metacognition.js +1 -0
- package/cc-soul/notify.js +88 -0
- package/cc-soul/patterns.js +1 -0
- package/cc-soul/persistence.js +147 -0
- package/cc-soul/persona.js +1 -0
- package/cc-soul/prompt-builder.js +322 -0
- package/cc-soul/quality.js +135 -0
- package/cc-soul/rover.js +1 -0
- package/cc-soul/sync.js +274 -0
- package/cc-soul/tasks.js +1 -0
- package/cc-soul/types.js +0 -0
- package/cc-soul/upgrade.js +1 -0
- package/cc-soul/user-profiles.js +1 -0
- package/cc-soul/values.js +1 -0
- package/cc-soul/voice.js +1 -0
- package/hub/dashboard.html +236 -0
- package/hub/package.json +16 -0
- package/hub/server.ts +447 -0
- package/package.json +29 -0
- package/scripts/cli.js +136 -0
- package/scripts/install.js +106 -0
package/hub/server.ts
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Hub — cc-soul 知识网络中心 (SQLite 版)
|
|
3
|
+
*
|
|
4
|
+
* 存储引擎:SQLite(单文件,支持索引,百万条毫秒级查询)
|
|
5
|
+
* 运行:npx tsx hub/server.ts
|
|
6
|
+
* 依赖:better-sqlite3(npm install better-sqlite3)
|
|
7
|
+
*
|
|
8
|
+
* API:
|
|
9
|
+
* POST /federation/upload ← cc-soul 实例上传知识
|
|
10
|
+
* GET /federation/download ← cc-soul 实例拉取知识
|
|
11
|
+
* GET /federation/stats ← 查看网络状态
|
|
12
|
+
* POST /federation/register ← 注册新 API key(需 admin key)
|
|
13
|
+
* POST /sync/push ← 跨设备同步上传(私有)
|
|
14
|
+
* GET /sync/pull ← 跨设备同步下载(私有)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'http'
|
|
18
|
+
import { mkdirSync, readFileSync } from 'fs'
|
|
19
|
+
import { resolve } from 'path'
|
|
20
|
+
import { createHash } from 'crypto'
|
|
21
|
+
import Database from 'better-sqlite3'
|
|
22
|
+
|
|
23
|
+
// ── Config ──
|
|
24
|
+
const PORT = parseInt(process.env.HUB_PORT || '9900')
|
|
25
|
+
const DATA_DIR = process.env.HUB_DATA || resolve(import.meta.dirname || '.', 'data')
|
|
26
|
+
const DB_PATH = resolve(DATA_DIR, 'hub.db')
|
|
27
|
+
const MIN_QUALITY = 5
|
|
28
|
+
|
|
29
|
+
// ── Init ──
|
|
30
|
+
mkdirSync(DATA_DIR, { recursive: true })
|
|
31
|
+
|
|
32
|
+
// ── Database ──
|
|
33
|
+
const db = new Database(DB_PATH)
|
|
34
|
+
db.pragma('journal_mode = WAL') // Write-Ahead Logging: 并发读不阻塞
|
|
35
|
+
db.pragma('busy_timeout = 5000')
|
|
36
|
+
|
|
37
|
+
// ── Schema ──
|
|
38
|
+
db.exec(`
|
|
39
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
40
|
+
key TEXT PRIMARY KEY,
|
|
41
|
+
instance_id TEXT NOT NULL,
|
|
42
|
+
instance_name TEXT DEFAULT '',
|
|
43
|
+
created_at INTEGER NOT NULL,
|
|
44
|
+
uploads INTEGER DEFAULT 0,
|
|
45
|
+
downloads INTEGER DEFAULT 0
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
49
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
50
|
+
content TEXT NOT NULL,
|
|
51
|
+
scope TEXT NOT NULL,
|
|
52
|
+
tags TEXT,
|
|
53
|
+
source_instance TEXT NOT NULL,
|
|
54
|
+
source_quality REAL DEFAULT 5.0,
|
|
55
|
+
timestamp INTEGER NOT NULL,
|
|
56
|
+
content_hash TEXT NOT NULL UNIQUE,
|
|
57
|
+
received_at INTEGER NOT NULL
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE IF NOT EXISTS sync_data (
|
|
61
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
62
|
+
instance_id TEXT NOT NULL,
|
|
63
|
+
data TEXT NOT NULL,
|
|
64
|
+
pushed_at INTEGER NOT NULL
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_memories_received ON memories(received_at);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source_instance);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(content_hash);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_sync_instance ON sync_data(instance_id);
|
|
71
|
+
`)
|
|
72
|
+
|
|
73
|
+
// Add reports column if not exists (safe for existing databases)
|
|
74
|
+
try { db.exec(`ALTER TABLE memories ADD COLUMN reports INTEGER DEFAULT 0`) } catch { /* already exists */ }
|
|
75
|
+
|
|
76
|
+
// ── Prepared statements (performance) ──
|
|
77
|
+
const stmts = {
|
|
78
|
+
insertMemory: db.prepare(`
|
|
79
|
+
INSERT OR IGNORE INTO memories (content, scope, tags, source_instance, source_quality, timestamp, content_hash, received_at)
|
|
80
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
81
|
+
`),
|
|
82
|
+
getMemories: db.prepare(`
|
|
83
|
+
SELECT * FROM memories WHERE received_at >= ? AND source_instance != ? ORDER BY received_at DESC LIMIT 1000
|
|
84
|
+
`),
|
|
85
|
+
countMemories: db.prepare(`SELECT COUNT(*) as count FROM memories`),
|
|
86
|
+
getApiKey: db.prepare(`SELECT * FROM api_keys WHERE key = ?`),
|
|
87
|
+
insertApiKey: db.prepare(`
|
|
88
|
+
INSERT INTO api_keys (key, instance_id, instance_name, created_at) VALUES (?, ?, ?, ?)
|
|
89
|
+
`),
|
|
90
|
+
updateApiKeyUploads: db.prepare(`UPDATE api_keys SET uploads = uploads + 1 WHERE key = ?`),
|
|
91
|
+
updateApiKeyDownloads: db.prepare(`UPDATE api_keys SET downloads = downloads + 1 WHERE key = ?`),
|
|
92
|
+
getAllApiKeys: db.prepare(`SELECT instance_name, uploads, downloads FROM api_keys`),
|
|
93
|
+
countInstances: db.prepare(`SELECT COUNT(*) as count FROM api_keys`),
|
|
94
|
+
upsertSync: db.prepare(`
|
|
95
|
+
INSERT INTO sync_data (instance_id, data, pushed_at) VALUES (?, ?, ?)
|
|
96
|
+
`),
|
|
97
|
+
getSyncData: db.prepare(`
|
|
98
|
+
SELECT data FROM sync_data WHERE instance_id != ? ORDER BY pushed_at DESC LIMIT 100
|
|
99
|
+
`),
|
|
100
|
+
cleanOldSync: db.prepare(`DELETE FROM sync_data WHERE pushed_at < ?`),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Batch insert transaction
|
|
104
|
+
const insertMemoriesBatch = db.transaction((memories: any[]) => {
|
|
105
|
+
let accepted = 0
|
|
106
|
+
for (const m of memories) {
|
|
107
|
+
const result = stmts.insertMemory.run(
|
|
108
|
+
m.content, m.scope, JSON.stringify(m.tags || []),
|
|
109
|
+
m.sourceInstance, m.sourceQuality,
|
|
110
|
+
m.timestamp, m.contentHash, Date.now()
|
|
111
|
+
)
|
|
112
|
+
if (result.changes > 0) accepted++
|
|
113
|
+
}
|
|
114
|
+
return accepted
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// ── PII filter (server-side double-check) ──
|
|
118
|
+
const PII_PATTERNS = [
|
|
119
|
+
/\b1[3-9]\d{9}\b/g,
|
|
120
|
+
/\b\d{17}[\dXx]\b/g,
|
|
121
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/gi,
|
|
122
|
+
/\b(?:sk-|api[_-]?key|token|secret|password)[=:]\s*\S+/gi,
|
|
123
|
+
/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
function hasPII(text: string): boolean {
|
|
127
|
+
return PII_PATTERNS.some(p => { p.lastIndex = 0; return p.test(text) })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function contentHash(s: string): string {
|
|
131
|
+
return createHash('sha256').update(s).digest('hex').slice(0, 16)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── HTTP helpers ──
|
|
135
|
+
const MAX_BODY_SIZE = 10 * 1024 * 1024 // 10MB max
|
|
136
|
+
|
|
137
|
+
async function readBody(req: IncomingMessage): Promise<string> {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const chunks: Buffer[] = []
|
|
140
|
+
let size = 0
|
|
141
|
+
req.on('data', (chunk: Buffer) => {
|
|
142
|
+
size += chunk.length
|
|
143
|
+
if (size > MAX_BODY_SIZE) {
|
|
144
|
+
req.destroy()
|
|
145
|
+
reject(new Error('body too large'))
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
chunks.push(chunk)
|
|
149
|
+
})
|
|
150
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
|
|
151
|
+
req.on('error', reject)
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function json(res: ServerResponse, status: number, data: any) {
|
|
156
|
+
res.writeHead(status, {
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
'Access-Control-Allow-Origin': '*',
|
|
159
|
+
})
|
|
160
|
+
res.end(JSON.stringify(data))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Routes ──
|
|
164
|
+
const server = createServer(async (req, res) => {
|
|
165
|
+
const url = new URL(req.url || '/', `http://localhost:${PORT}`)
|
|
166
|
+
const path = url.pathname
|
|
167
|
+
const method = req.method || 'GET'
|
|
168
|
+
|
|
169
|
+
// Dashboard
|
|
170
|
+
if (method === 'GET' && path === '/dashboard') {
|
|
171
|
+
try {
|
|
172
|
+
const html = readFileSync(resolve(import.meta.dirname || '.', 'dashboard.html'), 'utf-8')
|
|
173
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
174
|
+
return res.end(html)
|
|
175
|
+
} catch {
|
|
176
|
+
res.writeHead(404)
|
|
177
|
+
return res.end('dashboard.html not found')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// CORS preflight
|
|
182
|
+
if (method === 'OPTIONS') {
|
|
183
|
+
res.writeHead(200, {
|
|
184
|
+
'Access-Control-Allow-Origin': '*',
|
|
185
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
186
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Instance-Id, X-Quality-Score, X-Admin-Key',
|
|
187
|
+
})
|
|
188
|
+
return res.end()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
// ── Federation: Upload ──
|
|
193
|
+
if (method === 'POST' && path === '/federation/upload') {
|
|
194
|
+
const apiKey = (req.headers['authorization'] || '').replace('Bearer ', '')
|
|
195
|
+
const keyEntry = stmts.getApiKey.get(apiKey) as any
|
|
196
|
+
if (!keyEntry) return json(res, 401, { error: 'invalid api key' })
|
|
197
|
+
|
|
198
|
+
const quality = parseFloat(req.headers['x-quality-score'] as string || '0')
|
|
199
|
+
if (quality < MIN_QUALITY) {
|
|
200
|
+
return json(res, 403, { error: `quality too low: ${quality} < ${MIN_QUALITY}` })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const body = await readBody(req)
|
|
204
|
+
let data: any
|
|
205
|
+
try { data = JSON.parse(body) } catch { return json(res, 400, { error: 'invalid JSON body' }) }
|
|
206
|
+
|
|
207
|
+
// Filter and prepare
|
|
208
|
+
const valid = (data.memories || [])
|
|
209
|
+
.filter((m: any) => m.content && !hasPII(m.content))
|
|
210
|
+
.map((m: any) => ({
|
|
211
|
+
content: m.content,
|
|
212
|
+
scope: m.scope || 'fact',
|
|
213
|
+
tags: m.tags,
|
|
214
|
+
sourceInstance: m.sourceInstance || keyEntry.instance_id,
|
|
215
|
+
sourceQuality: quality,
|
|
216
|
+
timestamp: m.timestamp || Date.now(),
|
|
217
|
+
contentHash: m.contentHash || contentHash(m.content),
|
|
218
|
+
}))
|
|
219
|
+
|
|
220
|
+
const accepted = insertMemoriesBatch(valid)
|
|
221
|
+
stmts.updateApiKeyUploads.run(apiKey)
|
|
222
|
+
|
|
223
|
+
return json(res, 200, { accepted, total: (data.memories || []).length })
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Federation: Download ──
|
|
227
|
+
if (method === 'GET' && path === '/federation/download') {
|
|
228
|
+
const apiKey = (req.headers['authorization'] || '').replace('Bearer ', '')
|
|
229
|
+
const keyEntry = stmts.getApiKey.get(apiKey) as any
|
|
230
|
+
if (!keyEntry) return json(res, 401, { error: 'invalid api key' })
|
|
231
|
+
|
|
232
|
+
const since = parseInt(url.searchParams.get('since') || '0')
|
|
233
|
+
const instance = url.searchParams.get('instance') || keyEntry.instance_id
|
|
234
|
+
const memories = stmts.getMemories.all(since, instance) as any[]
|
|
235
|
+
|
|
236
|
+
// Parse tags back from JSON string
|
|
237
|
+
const formatted = memories.map(m => ({
|
|
238
|
+
...m,
|
|
239
|
+
tags: m.tags ? JSON.parse(m.tags) : [],
|
|
240
|
+
}))
|
|
241
|
+
|
|
242
|
+
stmts.updateApiKeyDownloads.run(apiKey)
|
|
243
|
+
return json(res, 200, { memories: formatted, count: formatted.length })
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Federation: Stats ──
|
|
247
|
+
if (method === 'GET' && path === '/federation/stats') {
|
|
248
|
+
const totalMemories = (stmts.countMemories.get() as any).count
|
|
249
|
+
const totalInstances = (stmts.countInstances.get() as any).count
|
|
250
|
+
const instances = stmts.getAllApiKeys.all() as any[]
|
|
251
|
+
|
|
252
|
+
return json(res, 200, {
|
|
253
|
+
totalMemories,
|
|
254
|
+
instances: totalInstances,
|
|
255
|
+
instanceList: instances,
|
|
256
|
+
dbSize: `${(db.pragma('page_count', { simple: true }) as number * db.pragma('page_size', { simple: true }) as number / 1024 / 1024).toFixed(2)} MB`,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Federation: Auto-Register (no admin key, for cc-soul clients) ──
|
|
261
|
+
if (method === 'POST' && path === '/federation/auto-register') {
|
|
262
|
+
const body = await readBody(req)
|
|
263
|
+
let data: any
|
|
264
|
+
try { data = JSON.parse(body) } catch { return json(res, 400, { error: 'invalid JSON body' }) }
|
|
265
|
+
|
|
266
|
+
const instanceId = data.instanceId
|
|
267
|
+
if (!instanceId) return json(res, 400, { error: 'missing instanceId' })
|
|
268
|
+
|
|
269
|
+
// Check if already registered
|
|
270
|
+
const existing = db.prepare(`SELECT key FROM api_keys WHERE instance_id = ?`).get(instanceId) as any
|
|
271
|
+
if (existing) {
|
|
272
|
+
return json(res, 200, { key: existing.key, instanceId, status: 'existing' })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Rate limit: max 100 registrations per day
|
|
276
|
+
const todayCount = (db.prepare(
|
|
277
|
+
`SELECT COUNT(*) as c FROM api_keys WHERE created_at > ?`
|
|
278
|
+
).get(Date.now() - 86400000) as any).c
|
|
279
|
+
if (todayCount >= 100) {
|
|
280
|
+
return json(res, 429, { error: 'too many registrations today, try tomorrow' })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const newKey = 'csk-' + createHash('sha256')
|
|
284
|
+
.update(Date.now() + Math.random().toString())
|
|
285
|
+
.digest('hex').slice(0, 32)
|
|
286
|
+
|
|
287
|
+
stmts.insertApiKey.run(
|
|
288
|
+
newKey,
|
|
289
|
+
instanceId,
|
|
290
|
+
data.instanceName || 'unnamed',
|
|
291
|
+
Date.now(),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
console.log(`[hub] auto-registered: ${instanceId} (${data.instanceName || 'unnamed'})`)
|
|
295
|
+
return json(res, 200, { key: newKey, instanceId, status: 'new' })
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── Federation: Register (admin) ──
|
|
299
|
+
if (method === 'POST' && path === '/federation/register') {
|
|
300
|
+
const adminKey = req.headers['x-admin-key'] as string
|
|
301
|
+
if (adminKey !== process.env.HUB_ADMIN_KEY) {
|
|
302
|
+
return json(res, 401, { error: 'invalid admin key' })
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const body = await readBody(req)
|
|
306
|
+
let data: any
|
|
307
|
+
try { data = JSON.parse(body) } catch { return json(res, 400, { error: 'invalid JSON body' }) }
|
|
308
|
+
const newKey = 'csk-' + createHash('sha256')
|
|
309
|
+
.update(Date.now() + Math.random().toString())
|
|
310
|
+
.digest('hex').slice(0, 32)
|
|
311
|
+
|
|
312
|
+
stmts.insertApiKey.run(
|
|
313
|
+
newKey,
|
|
314
|
+
data.instanceId || 'unknown',
|
|
315
|
+
data.instanceName || 'unnamed',
|
|
316
|
+
Date.now(),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return json(res, 200, { key: newKey, instanceId: data.instanceId })
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Sync: Push ──
|
|
323
|
+
if (method === 'POST' && path === '/sync/push') {
|
|
324
|
+
const instanceId = req.headers['x-instance-id'] as string
|
|
325
|
+
if (!instanceId) return json(res, 400, { error: 'missing X-Instance-Id' })
|
|
326
|
+
|
|
327
|
+
const body = await readBody(req)
|
|
328
|
+
|
|
329
|
+
// Validate JSONL format: each non-empty line must be valid JSON
|
|
330
|
+
const lines = body.split('\n').filter(l => l.trim())
|
|
331
|
+
const validLines: string[] = []
|
|
332
|
+
for (const line of lines) {
|
|
333
|
+
try { JSON.parse(line); validLines.push(line) } catch { /* skip malformed lines */ }
|
|
334
|
+
}
|
|
335
|
+
const validBody = validLines.join('\n')
|
|
336
|
+
|
|
337
|
+
// Clean old sync data for this instance (keep latest only)
|
|
338
|
+
stmts.cleanOldSync.run(Date.now() - 7 * 86400000) // clean >7 days old
|
|
339
|
+
stmts.upsertSync.run(instanceId, validBody, Date.now())
|
|
340
|
+
|
|
341
|
+
return json(res, 200, { ok: true, lines: validLines.length, dropped: lines.length - validLines.length })
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Sync: Pull ──
|
|
345
|
+
if (method === 'GET' && path === '/sync/pull') {
|
|
346
|
+
const instanceId = req.headers['x-instance-id'] as string || url.searchParams.get('instance') || ''
|
|
347
|
+
if (!instanceId) return json(res, 400, { error: 'missing instance id' })
|
|
348
|
+
|
|
349
|
+
const rows = stmts.getSyncData.all(instanceId) as any[]
|
|
350
|
+
const allData = rows.map(r => r.data).join('\n')
|
|
351
|
+
|
|
352
|
+
res.writeHead(200, { 'Content-Type': 'application/jsonl' })
|
|
353
|
+
return res.end(allData)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Health check ──
|
|
357
|
+
if (method === 'GET' && (path === '/' || path === '/health')) {
|
|
358
|
+
return json(res, 200, {
|
|
359
|
+
status: 'ok',
|
|
360
|
+
version: '1.0.0',
|
|
361
|
+
uptime: process.uptime(),
|
|
362
|
+
memories: (stmts.countMemories.get() as any).count,
|
|
363
|
+
instances: (stmts.countInstances.get() as any).count,
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ── Federation: Report bad knowledge ──
|
|
368
|
+
if (method === 'POST' && path === '/federation/report') {
|
|
369
|
+
const apiKey = (req.headers['authorization'] || '').replace('Bearer ', '')
|
|
370
|
+
const keyEntry = stmts.getApiKey.get(apiKey) as any
|
|
371
|
+
if (!keyEntry) return json(res, 401, { error: 'invalid api key' })
|
|
372
|
+
|
|
373
|
+
const body = await readBody(req)
|
|
374
|
+
let data: any
|
|
375
|
+
try { data = JSON.parse(body) } catch { return json(res, 400, { error: 'invalid JSON body' }) }
|
|
376
|
+
const hash = data.contentHash
|
|
377
|
+
|
|
378
|
+
if (!hash) return json(res, 400, { error: 'missing contentHash' })
|
|
379
|
+
|
|
380
|
+
const updateReport = db.prepare(`UPDATE memories SET reports = reports + 1 WHERE content_hash = ?`)
|
|
381
|
+
updateReport.run(hash)
|
|
382
|
+
|
|
383
|
+
// Auto-delete if reported 3+ times
|
|
384
|
+
const mem = db.prepare(`SELECT reports FROM memories WHERE content_hash = ?`).get(hash) as any
|
|
385
|
+
if (mem && mem.reports >= 3) {
|
|
386
|
+
db.prepare(`DELETE FROM memories WHERE content_hash = ?`).run(hash)
|
|
387
|
+
console.log(`[hub] auto-deleted memory ${hash} (${mem.reports} reports)`)
|
|
388
|
+
return json(res, 200, { action: 'deleted', reports: mem.reports })
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return json(res, 200, { action: 'reported', reports: mem?.reports || 1 })
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── 404 ──
|
|
395
|
+
json(res, 404, { error: 'not found', routes: [
|
|
396
|
+
'GET /health',
|
|
397
|
+
'POST /federation/upload',
|
|
398
|
+
'GET /federation/download',
|
|
399
|
+
'GET /federation/stats',
|
|
400
|
+
'POST /federation/register',
|
|
401
|
+
'POST /federation/report',
|
|
402
|
+
'POST /sync/push',
|
|
403
|
+
'GET /sync/pull',
|
|
404
|
+
]})
|
|
405
|
+
|
|
406
|
+
} catch (e: any) {
|
|
407
|
+
console.error(`[hub] ${method} ${path} error: ${e.message}`)
|
|
408
|
+
json(res, 500, { error: e.message })
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
// ── Graceful shutdown ──
|
|
413
|
+
process.on('SIGINT', () => {
|
|
414
|
+
console.log('\n[hub] shutting down...')
|
|
415
|
+
db.close()
|
|
416
|
+
process.exit(0)
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
process.on('SIGTERM', () => {
|
|
420
|
+
db.close()
|
|
421
|
+
process.exit(0)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
server.listen(PORT, () => {
|
|
425
|
+
const memCount = (stmts.countMemories.get() as any).count
|
|
426
|
+
const instanceCount = (stmts.countInstances.get() as any).count
|
|
427
|
+
|
|
428
|
+
console.log(`
|
|
429
|
+
🧠 cc-soul Knowledge Hub (SQLite)
|
|
430
|
+
listening on http://0.0.0.0:${PORT}
|
|
431
|
+
database: ${DB_PATH}
|
|
432
|
+
memories: ${memCount}
|
|
433
|
+
instances: ${instanceCount}
|
|
434
|
+
|
|
435
|
+
API:
|
|
436
|
+
GET /health ← 健康检查
|
|
437
|
+
POST /federation/upload ← 实例上传知识
|
|
438
|
+
GET /federation/download ← 实例下载知识
|
|
439
|
+
GET /federation/stats ← 网络状态
|
|
440
|
+
POST /federation/register ← 注册 API key(需 admin key)
|
|
441
|
+
POST /federation/report ← 举报错误知识
|
|
442
|
+
POST /sync/push ← 跨设备同步上传
|
|
443
|
+
GET /sync/pull ← 跨设备同步下载
|
|
444
|
+
|
|
445
|
+
env: HUB_PORT=${PORT} HUB_ADMIN_KEY=${process.env.HUB_ADMIN_KEY ? '(set)' : '⚠️ NOT SET'}
|
|
446
|
+
`)
|
|
447
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cc-soul/openclaw",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Give your AI a soul — cognitive architecture plugin for OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": ["ai", "soul", "memory", "personality", "openclaw", "cognitive", "agent"],
|
|
7
|
+
"author": "cc-soul",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/cc-soul/cc-soul"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"cc-soul": "./scripts/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"cc-soul/",
|
|
18
|
+
"hub/",
|
|
19
|
+
"data/.gitkeep",
|
|
20
|
+
"scripts/",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"openclaw": {
|
|
24
|
+
"hooks": ["./cc-soul"]
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"postinstall": "node scripts/install.js"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/scripts/cli.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cc-soul CLI — enable/disable/status/config commands
|
|
4
|
+
* Usage: cc-soul <command> [args]
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
7
|
+
import { resolve } from 'path'
|
|
8
|
+
import { homedir } from 'os'
|
|
9
|
+
|
|
10
|
+
const DATA_DIR = resolve(homedir(), '.openclaw/hooks/cc-soul/data')
|
|
11
|
+
const FEATURES_PATH = resolve(DATA_DIR, 'features.json')
|
|
12
|
+
const SYNC_PATH = resolve(DATA_DIR, 'sync_config.json')
|
|
13
|
+
const AI_PATH = resolve(DATA_DIR, 'ai_config.json')
|
|
14
|
+
|
|
15
|
+
function loadJson(path, fallback) {
|
|
16
|
+
try { return existsSync(path) ? JSON.parse(readFileSync(path, 'utf-8')) : fallback }
|
|
17
|
+
catch { return fallback }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const [,, cmd, ...args] = process.argv
|
|
21
|
+
|
|
22
|
+
switch (cmd) {
|
|
23
|
+
case 'status': {
|
|
24
|
+
const features = loadJson(FEATURES_PATH, {})
|
|
25
|
+
const sync = loadJson(SYNC_PATH, {})
|
|
26
|
+
console.log('\n🧠 cc-soul status\n')
|
|
27
|
+
console.log('Features:')
|
|
28
|
+
for (const [k, v] of Object.entries(features)) {
|
|
29
|
+
if (k.startsWith('_')) continue
|
|
30
|
+
console.log(` ${v ? '✅' : '❌'} ${k}`)
|
|
31
|
+
}
|
|
32
|
+
console.log(`\nSync: ${sync.enabled ? 'ON' : 'OFF'}`)
|
|
33
|
+
console.log(`Federation: ${sync.federationEnabled ? 'ON' : 'OFF'}`)
|
|
34
|
+
console.log(`Instance: ${sync.instanceId || 'not set'}`)
|
|
35
|
+
console.log(`Hub: ${sync.hubUrl || 'not configured'}`)
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case 'enable': {
|
|
40
|
+
if (!args[0]) { console.log('Usage: cc-soul enable <feature>'); break }
|
|
41
|
+
const features = loadJson(FEATURES_PATH, {})
|
|
42
|
+
if (!(args[0] in features)) { console.log(`Unknown feature: ${args[0]}`); break }
|
|
43
|
+
features[args[0]] = true
|
|
44
|
+
writeFileSync(FEATURES_PATH, JSON.stringify(features, null, 2))
|
|
45
|
+
console.log(`✅ ${args[0]} enabled`)
|
|
46
|
+
console.log('Restart gateway: pkill -HUP -f "openclaw.*gateway"')
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case 'disable': {
|
|
51
|
+
if (!args[0]) { console.log('Usage: cc-soul disable <feature>'); break }
|
|
52
|
+
const features = loadJson(FEATURES_PATH, {})
|
|
53
|
+
if (!(args[0] in features)) { console.log(`Unknown feature: ${args[0]}`); break }
|
|
54
|
+
features[args[0]] = false
|
|
55
|
+
writeFileSync(FEATURES_PATH, JSON.stringify(features, null, 2))
|
|
56
|
+
console.log(`❌ ${args[0]} disabled`)
|
|
57
|
+
console.log('Restart gateway: pkill -HUP -f "openclaw.*gateway"')
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case 'config': {
|
|
62
|
+
const [subCmd, key, value] = args
|
|
63
|
+
if (subCmd === 'set' && key && value) {
|
|
64
|
+
if (['hub_url', 'hubUrl'].includes(key)) {
|
|
65
|
+
const sync = loadJson(SYNC_PATH, {})
|
|
66
|
+
sync.hubUrl = value
|
|
67
|
+
sync.enabled = true
|
|
68
|
+
writeFileSync(SYNC_PATH, JSON.stringify(sync, null, 2))
|
|
69
|
+
console.log(`✅ hubUrl = ${value}`)
|
|
70
|
+
} else if (['federation', 'federationEnabled'].includes(key)) {
|
|
71
|
+
const sync = loadJson(SYNC_PATH, {})
|
|
72
|
+
sync.federationEnabled = value === 'true'
|
|
73
|
+
writeFileSync(SYNC_PATH, JSON.stringify(sync, null, 2))
|
|
74
|
+
console.log(`✅ federation = ${value}`)
|
|
75
|
+
} else if (['ai_backend', 'backend'].includes(key)) {
|
|
76
|
+
const ai = loadJson(AI_PATH, {})
|
|
77
|
+
ai.backend = value
|
|
78
|
+
writeFileSync(AI_PATH, JSON.stringify(ai, null, 2))
|
|
79
|
+
console.log(`✅ AI backend = ${value}`)
|
|
80
|
+
} else if (['ai_api_base', 'api_base'].includes(key)) {
|
|
81
|
+
const ai = loadJson(AI_PATH, {})
|
|
82
|
+
ai.backend = 'openai-compatible'
|
|
83
|
+
ai.api_base = value
|
|
84
|
+
writeFileSync(AI_PATH, JSON.stringify(ai, null, 2))
|
|
85
|
+
console.log(`✅ API base = ${value}`)
|
|
86
|
+
} else if (['ai_api_key', 'api_key'].includes(key)) {
|
|
87
|
+
const ai = loadJson(AI_PATH, {})
|
|
88
|
+
ai.api_key = value
|
|
89
|
+
writeFileSync(AI_PATH, JSON.stringify(ai, null, 2))
|
|
90
|
+
console.log(`✅ API key set`)
|
|
91
|
+
} else if (['ai_model', 'model'].includes(key)) {
|
|
92
|
+
const ai = loadJson(AI_PATH, {})
|
|
93
|
+
ai.api_model = value
|
|
94
|
+
writeFileSync(AI_PATH, JSON.stringify(ai, null, 2))
|
|
95
|
+
console.log(`✅ model = ${value}`)
|
|
96
|
+
} else {
|
|
97
|
+
console.log(`Unknown config key: ${key}`)
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
console.log('Usage: cc-soul config set <key> <value>')
|
|
101
|
+
console.log('Keys: hub_url, federation, ai_backend, ai_api_base, ai_api_key, ai_model')
|
|
102
|
+
}
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'install': {
|
|
107
|
+
console.log('Running install script...')
|
|
108
|
+
import('./install.js')
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
default:
|
|
113
|
+
console.log(`
|
|
114
|
+
🧠 cc-soul — Give your AI a soul
|
|
115
|
+
|
|
116
|
+
Commands:
|
|
117
|
+
cc-soul status Show features & sync status
|
|
118
|
+
cc-soul enable <feature> Enable a feature
|
|
119
|
+
cc-soul disable <feature> Disable a feature
|
|
120
|
+
cc-soul config set <key> <value> Set configuration
|
|
121
|
+
|
|
122
|
+
Config keys:
|
|
123
|
+
hub_url <url> Knowledge Hub URL
|
|
124
|
+
federation true/false Enable/disable federation
|
|
125
|
+
ai_backend cli/openai-compatible
|
|
126
|
+
ai_api_base <url> API base URL
|
|
127
|
+
ai_api_key <key> API key
|
|
128
|
+
ai_model <model> Model name
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
cc-soul enable dream_mode
|
|
132
|
+
cc-soul disable self_upgrade
|
|
133
|
+
cc-soul config set hub_url https://hub.example.com:9900
|
|
134
|
+
cc-soul config set ai_api_base https://api.openai.com/v1
|
|
135
|
+
`)
|
|
136
|
+
}
|