@zshuangmu/agenthub 0.4.14 → 0.4.16

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 (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +268 -268
  3. package/package.json +41 -41
  4. package/src/api-server.js +518 -244
  5. package/src/cli.js +714 -671
  6. package/src/commands/api.js +9 -9
  7. package/src/commands/doctor.js +335 -335
  8. package/src/commands/info.js +15 -15
  9. package/src/commands/install.js +56 -56
  10. package/src/commands/list.js +78 -78
  11. package/src/commands/pack.js +249 -156
  12. package/src/commands/publish-remote.js +9 -9
  13. package/src/commands/publish.js +7 -7
  14. package/src/commands/rollback.js +59 -59
  15. package/src/commands/search.js +14 -14
  16. package/src/commands/serve.js +9 -9
  17. package/src/commands/stats.js +105 -105
  18. package/src/commands/uninstall.js +76 -76
  19. package/src/commands/update.js +54 -54
  20. package/src/commands/verify.js +133 -133
  21. package/src/commands/versions.js +75 -75
  22. package/src/commands/web.js +9 -9
  23. package/src/index.js +18 -18
  24. package/src/lib/auth.js +301 -0
  25. package/src/lib/bundle-transfer.js +58 -58
  26. package/src/lib/colors.js +60 -60
  27. package/src/lib/database.js +450 -244
  28. package/src/lib/debug.js +135 -135
  29. package/src/lib/fs-utils.js +107 -50
  30. package/src/lib/html.js +2163 -1824
  31. package/src/lib/http.js +168 -168
  32. package/src/lib/install.js +60 -60
  33. package/src/lib/manifest.js +124 -124
  34. package/src/lib/openclaw-config.js +40 -40
  35. package/src/lib/permissions.js +105 -0
  36. package/src/lib/privacy-engine.js +220 -0
  37. package/src/lib/registry.js +130 -130
  38. package/src/lib/remote.js +11 -11
  39. package/src/lib/security-scanner.js +233 -233
  40. package/src/lib/signing.js +158 -0
  41. package/src/lib/version-manager.js +77 -77
  42. package/src/server.js +176 -176
  43. package/src/web-server.js +135 -135
@@ -1,244 +1,450 @@
1
- import path from "node:path";
2
- import { pathExists, ensureDir } from "./fs-utils.js";
3
- import { readFile, writeFile } from "node:fs/promises";
4
- import initSqlJs from "sql.js";
5
-
6
- let db = null;
7
- let dbPath = null;
8
-
9
- /**
10
- * 初始化数据库
11
- */
12
- export async function initDatabase(registryDir) {
13
- if (db) return db;
14
-
15
- const SQL = await initSqlJs();
16
- dbPath = path.join(registryDir, "agenthub.db");
17
-
18
- // 确保目录存在
19
- await ensureDir(registryDir);
20
-
21
- // 尝试加载现有数据库
22
- if (await pathExists(dbPath)) {
23
- const buffer = await readFile(dbPath);
24
- db = new SQL.Database(buffer);
25
- } else {
26
- db = new SQL.Database();
27
- }
28
-
29
- // 创建表
30
- db.run(`
31
- CREATE TABLE IF NOT EXISTS download_stats (
32
- id INTEGER PRIMARY KEY AUTOINCREMENT,
33
- agent_slug TEXT NOT NULL UNIQUE,
34
- downloads INTEGER DEFAULT 0,
35
- last_download_at TEXT,
36
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
37
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP
38
- )
39
- `);
40
-
41
- db.run(`
42
- CREATE TABLE IF NOT EXISTS download_logs (
43
- id INTEGER PRIMARY KEY AUTOINCREMENT,
44
- agent_slug TEXT NOT NULL,
45
- installed_at TEXT DEFAULT CURRENT_TIMESTAMP,
46
- target_workspace TEXT,
47
- ip_address TEXT,
48
- user_agent TEXT
49
- )
50
- `);
51
-
52
- // 创建索引
53
- db.run(`CREATE INDEX IF NOT EXISTS idx_download_stats_slug ON download_stats(agent_slug)`);
54
- db.run(`CREATE INDEX IF NOT EXISTS idx_download_logs_slug ON download_logs(agent_slug)`);
55
-
56
- await saveDatabase();
57
- return db;
58
- }
59
-
60
- /**
61
- * 保存数据库到文件
62
- */
63
- export async function saveDatabase() {
64
- if (!db || !dbPath) return;
65
- const data = db.export();
66
- const buffer = Buffer.from(data);
67
- await writeFile(dbPath, buffer);
68
- }
69
-
70
- /**
71
- * 关闭数据库连接
72
- */
73
- export async function closeDatabase() {
74
- if (db) {
75
- await saveDatabase();
76
- db.close();
77
- db = null;
78
- }
79
- }
80
-
81
- /**
82
- * 增加下载次数
83
- */
84
- export async function incrementDownloads(registryDir, slug, metadata = {}) {
85
- await initDatabase(registryDir);
86
-
87
- // 更新或插入下载统计
88
- const existing = db.exec(`SELECT downloads FROM download_stats WHERE agent_slug = ?`, [slug]);
89
-
90
- if (existing.length > 0 && existing[0].values.length > 0) {
91
- db.run(`
92
- UPDATE download_stats
93
- SET downloads = downloads + 1,
94
- last_download_at = CURRENT_TIMESTAMP,
95
- updated_at = CURRENT_TIMESTAMP
96
- WHERE agent_slug = ?
97
- `, [slug]);
98
- } else {
99
- db.run(`
100
- INSERT INTO download_stats (agent_slug, downloads, last_download_at, created_at, updated_at)
101
- VALUES (?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
102
- `, [slug]);
103
- }
104
-
105
- // 记录下载日志
106
- db.run(`
107
- INSERT INTO download_logs (agent_slug, target_workspace, ip_address, user_agent)
108
- VALUES (?, ?, ?, ?)
109
- `, [slug, metadata.targetWorkspace || null, metadata.ip || null, metadata.userAgent || null]);
110
-
111
- await saveDatabase();
112
-
113
- return getAgentDownloads(registryDir, slug);
114
- }
115
-
116
- /**
117
- * 获取单个 Agent 的下载次数
118
- */
119
- export async function getAgentDownloads(registryDir, slug) {
120
- await initDatabase(registryDir);
121
-
122
- const result = db.exec(`SELECT downloads FROM download_stats WHERE agent_slug = ?`, [slug]);
123
- if (result.length > 0 && result[0].values.length > 0) {
124
- return result[0].values[0][0];
125
- }
126
- return 0;
127
- }
128
-
129
- /**
130
- * 批量获取多个 Agent 的下载次数
131
- */
132
- export async function getAgentsDownloads(registryDir, slugs) {
133
- await initDatabase(registryDir);
134
-
135
- const result = {};
136
- for (const slug of slugs) {
137
- result[slug] = 0;
138
- }
139
-
140
- const placeholders = slugs.map(() => '?').join(',');
141
- const rows = db.exec(`SELECT agent_slug, downloads FROM download_stats WHERE agent_slug IN (${placeholders})`, slugs);
142
-
143
- if (rows.length > 0) {
144
- for (const row of rows[0].values) {
145
- result[row[0]] = row[1];
146
- }
147
- }
148
-
149
- return result;
150
- }
151
-
152
- /**
153
- * 获取所有 Agent 的下载次数
154
- */
155
- export async function getAllDownloads(registryDir) {
156
- await initDatabase(registryDir);
157
-
158
- const result = {};
159
- const rows = db.exec(`SELECT agent_slug, downloads FROM download_stats`);
160
-
161
- if (rows.length > 0) {
162
- for (const row of rows[0].values) {
163
- result[row[0]] = row[1];
164
- }
165
- }
166
-
167
- return result;
168
- }
169
-
170
- /**
171
- * 获取总下载次数
172
- */
173
- export async function getTotalDownloads(registryDir) {
174
- await initDatabase(registryDir);
175
-
176
- const result = db.exec(`SELECT COALESCE(SUM(downloads), 0) as total FROM download_stats`);
177
- if (result.length > 0 && result[0].values.length > 0) {
178
- return result[0].values[0][0];
179
- }
180
- return 0;
181
- }
182
-
183
- /**
184
- * 获取下载排行
185
- */
186
- export async function getDownloadRanking(registryDir, limit = 10) {
187
- await initDatabase(registryDir);
188
-
189
- const rows = db.exec(`
190
- SELECT agent_slug, downloads, last_download_at
191
- FROM download_stats
192
- ORDER BY downloads DESC
193
- LIMIT ?
194
- `, [limit]);
195
-
196
- if (rows.length > 0) {
197
- return rows[0].values.map(row => ({
198
- slug: row[0],
199
- downloads: row[1],
200
- lastDownload: row[2]
201
- }));
202
- }
203
- return [];
204
- }
205
-
206
- /**
207
- * 获取最近的下载日志
208
- */
209
- export async function getRecentDownloads(registryDir, limit = 50) {
210
- await initDatabase(registryDir);
211
-
212
- const rows = db.exec(`
213
- SELECT agent_slug, installed_at, target_workspace
214
- FROM download_logs
215
- ORDER BY installed_at DESC
216
- LIMIT ?
217
- `, [limit]);
218
-
219
- if (rows.length > 0) {
220
- return rows[0].values.map(row => ({
221
- slug: row[0],
222
- installedAt: row[1],
223
- targetWorkspace: row[2]
224
- }));
225
- }
226
- return [];
227
- }
228
-
229
- /**
230
- * 获取数据库统计信息
231
- */
232
- export async function getDatabaseStats(registryDir) {
233
- await initDatabase(registryDir);
234
-
235
- const totalAgents = db.exec(`SELECT COUNT(*) FROM download_stats`)[0]?.values[0][0] || 0;
236
- const totalDownloads = await getTotalDownloads(registryDir);
237
- const totalLogs = db.exec(`SELECT COUNT(*) FROM download_logs`)[0]?.values[0][0] || 0;
238
-
239
- return {
240
- totalAgents,
241
- totalDownloads,
242
- totalLogs
243
- };
244
- }
1
+ import path from "node:path";
2
+ import { pathExists, ensureDir } from "./fs-utils.js";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import initSqlJs from "sql.js";
5
+
6
+ let db = null;
7
+ let dbPath = null;
8
+
9
+ /**
10
+ * 初始化数据库
11
+ */
12
+ export async function initDatabase(registryDir) {
13
+ if (db) return db;
14
+
15
+ const SQL = await initSqlJs();
16
+ dbPath = path.join(registryDir, "agenthub.db");
17
+
18
+ // 确保目录存在
19
+ await ensureDir(registryDir);
20
+
21
+ // 尝试加载现有数据库
22
+ if (await pathExists(dbPath)) {
23
+ const buffer = await readFile(dbPath);
24
+ db = new SQL.Database(buffer);
25
+ } else {
26
+ db = new SQL.Database();
27
+ }
28
+
29
+ // 创建表
30
+ db.run(`
31
+ CREATE TABLE IF NOT EXISTS download_stats (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ agent_slug TEXT NOT NULL UNIQUE,
34
+ downloads INTEGER DEFAULT 0,
35
+ last_download_at TEXT,
36
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
37
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
38
+ )
39
+ `);
40
+
41
+ db.run(`
42
+ CREATE TABLE IF NOT EXISTS download_logs (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ agent_slug TEXT NOT NULL,
45
+ installed_at TEXT DEFAULT CURRENT_TIMESTAMP,
46
+ target_workspace TEXT,
47
+ ip_address TEXT,
48
+ user_agent TEXT
49
+ )
50
+ `);
51
+
52
+ // 创建索引
53
+ db.run(`CREATE INDEX IF NOT EXISTS idx_download_stats_slug ON download_stats(agent_slug)`);
54
+ db.run(`CREATE INDEX IF NOT EXISTS idx_download_logs_slug ON download_logs(agent_slug)`);
55
+
56
+ // === P1: 用户认证与权限表 ===
57
+
58
+ db.run(`
59
+ CREATE TABLE IF NOT EXISTS users (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ username TEXT NOT NULL UNIQUE,
62
+ password_hash TEXT NOT NULL,
63
+ email TEXT,
64
+ role TEXT DEFAULT 'user',
65
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
66
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
67
+ )
68
+ `);
69
+
70
+ db.run(`
71
+ CREATE TABLE IF NOT EXISTS api_tokens (
72
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
73
+ user_id INTEGER NOT NULL,
74
+ token_hash TEXT NOT NULL UNIQUE,
75
+ label TEXT DEFAULT 'default',
76
+ scopes TEXT DEFAULT 'read:agent,publish',
77
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
78
+ expires_at TEXT,
79
+ revoked_at TEXT,
80
+ FOREIGN KEY (user_id) REFERENCES users(id)
81
+ )
82
+ `);
83
+
84
+ db.run(`
85
+ CREATE TABLE IF NOT EXISTS audit_logs (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ user_id INTEGER,
88
+ username TEXT,
89
+ action TEXT NOT NULL,
90
+ resource TEXT,
91
+ details TEXT,
92
+ ip_address TEXT,
93
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
94
+ )
95
+ `);
96
+
97
+ db.run(`CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)`);
98
+ db.run(`CREATE INDEX IF NOT EXISTS idx_api_tokens_hash ON api_tokens(token_hash)`);
99
+ db.run(`CREATE INDEX IF NOT EXISTS idx_audit_logs_user ON audit_logs(user_id)`);
100
+ db.run(`CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action)`);
101
+
102
+ await saveDatabase();
103
+ return db;
104
+ }
105
+
106
+ /**
107
+ * 保存数据库到文件
108
+ */
109
+ export async function saveDatabase() {
110
+ if (!db || !dbPath) return;
111
+ const data = db.export();
112
+ const buffer = Buffer.from(data);
113
+ await writeFile(dbPath, buffer);
114
+ }
115
+
116
+ /**
117
+ * 关闭数据库连接
118
+ */
119
+ export async function closeDatabase() {
120
+ if (db) {
121
+ await saveDatabase();
122
+ db.close();
123
+ db = null;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * 增加下载次数
129
+ */
130
+ export async function incrementDownloads(registryDir, slug, metadata = {}) {
131
+ await initDatabase(registryDir);
132
+
133
+ // 更新或插入下载统计
134
+ const existing = db.exec(`SELECT downloads FROM download_stats WHERE agent_slug = ?`, [slug]);
135
+
136
+ if (existing.length > 0 && existing[0].values.length > 0) {
137
+ db.run(`
138
+ UPDATE download_stats
139
+ SET downloads = downloads + 1,
140
+ last_download_at = CURRENT_TIMESTAMP,
141
+ updated_at = CURRENT_TIMESTAMP
142
+ WHERE agent_slug = ?
143
+ `, [slug]);
144
+ } else {
145
+ db.run(`
146
+ INSERT INTO download_stats (agent_slug, downloads, last_download_at, created_at, updated_at)
147
+ VALUES (?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
148
+ `, [slug]);
149
+ }
150
+
151
+ // 记录下载日志
152
+ db.run(`
153
+ INSERT INTO download_logs (agent_slug, target_workspace, ip_address, user_agent)
154
+ VALUES (?, ?, ?, ?)
155
+ `, [slug, metadata.targetWorkspace || null, metadata.ip || null, metadata.userAgent || null]);
156
+
157
+ await saveDatabase();
158
+
159
+ return getAgentDownloads(registryDir, slug);
160
+ }
161
+
162
+ /**
163
+ * 获取单个 Agent 的下载次数
164
+ */
165
+ export async function getAgentDownloads(registryDir, slug) {
166
+ await initDatabase(registryDir);
167
+
168
+ const result = db.exec(`SELECT downloads FROM download_stats WHERE agent_slug = ?`, [slug]);
169
+ if (result.length > 0 && result[0].values.length > 0) {
170
+ return result[0].values[0][0];
171
+ }
172
+ return 0;
173
+ }
174
+
175
+ /**
176
+ * 批量获取多个 Agent 的下载次数
177
+ */
178
+ export async function getAgentsDownloads(registryDir, slugs) {
179
+ await initDatabase(registryDir);
180
+
181
+ const result = {};
182
+ for (const slug of slugs) {
183
+ result[slug] = 0;
184
+ }
185
+
186
+ const placeholders = slugs.map(() => '?').join(',');
187
+ const rows = db.exec(`SELECT agent_slug, downloads FROM download_stats WHERE agent_slug IN (${placeholders})`, slugs);
188
+
189
+ if (rows.length > 0) {
190
+ for (const row of rows[0].values) {
191
+ result[row[0]] = row[1];
192
+ }
193
+ }
194
+
195
+ return result;
196
+ }
197
+
198
+ /**
199
+ * 获取所有 Agent 的下载次数
200
+ */
201
+ export async function getAllDownloads(registryDir) {
202
+ await initDatabase(registryDir);
203
+
204
+ const result = {};
205
+ const rows = db.exec(`SELECT agent_slug, downloads FROM download_stats`);
206
+
207
+ if (rows.length > 0) {
208
+ for (const row of rows[0].values) {
209
+ result[row[0]] = row[1];
210
+ }
211
+ }
212
+
213
+ return result;
214
+ }
215
+
216
+ /**
217
+ * 获取总下载次数
218
+ */
219
+ export async function getTotalDownloads(registryDir) {
220
+ await initDatabase(registryDir);
221
+
222
+ const result = db.exec(`SELECT COALESCE(SUM(downloads), 0) as total FROM download_stats`);
223
+ if (result.length > 0 && result[0].values.length > 0) {
224
+ return result[0].values[0][0];
225
+ }
226
+ return 0;
227
+ }
228
+
229
+ /**
230
+ * 获取下载排行
231
+ */
232
+ export async function getDownloadRanking(registryDir, limit = 10) {
233
+ await initDatabase(registryDir);
234
+
235
+ const rows = db.exec(`
236
+ SELECT agent_slug, downloads, last_download_at
237
+ FROM download_stats
238
+ ORDER BY downloads DESC
239
+ LIMIT ?
240
+ `, [limit]);
241
+
242
+ if (rows.length > 0) {
243
+ return rows[0].values.map(row => ({
244
+ slug: row[0],
245
+ downloads: row[1],
246
+ lastDownload: row[2]
247
+ }));
248
+ }
249
+ return [];
250
+ }
251
+
252
+ /**
253
+ * 获取最近的下载日志
254
+ */
255
+ export async function getRecentDownloads(registryDir, limit = 50) {
256
+ await initDatabase(registryDir);
257
+
258
+ const rows = db.exec(`
259
+ SELECT agent_slug, installed_at, target_workspace
260
+ FROM download_logs
261
+ ORDER BY installed_at DESC
262
+ LIMIT ?
263
+ `, [limit]);
264
+
265
+ if (rows.length > 0) {
266
+ return rows[0].values.map(row => ({
267
+ slug: row[0],
268
+ installedAt: row[1],
269
+ targetWorkspace: row[2]
270
+ }));
271
+ }
272
+ return [];
273
+ }
274
+
275
+ /**
276
+ * 获取数据库统计信息
277
+ */
278
+ export async function getDatabaseStats(registryDir) {
279
+ await initDatabase(registryDir);
280
+
281
+ const totalAgents = db.exec(`SELECT COUNT(*) FROM download_stats`)[0]?.values[0][0] || 0;
282
+ const totalDownloads = await getTotalDownloads(registryDir);
283
+ const totalLogs = db.exec(`SELECT COUNT(*) FROM download_logs`)[0]?.values[0][0] || 0;
284
+
285
+ return {
286
+ totalAgents,
287
+ totalDownloads,
288
+ totalLogs
289
+ };
290
+ }
291
+
292
+ // === P1: 用户管理 ===
293
+
294
+ /**
295
+ * 创建用户
296
+ */
297
+ export async function createUser(registryDir, { username, passwordHash, email, role = "user" }) {
298
+ await initDatabase(registryDir);
299
+ db.run(
300
+ `INSERT INTO users (username, password_hash, email, role) VALUES (?, ?, ?, ?)`,
301
+ [username, passwordHash, email || null, role]
302
+ );
303
+ await saveDatabase();
304
+ const result = db.exec(`SELECT id, username, email, role, created_at FROM users WHERE username = ?`, [username]);
305
+ if (result.length > 0 && result[0].values.length > 0) {
306
+ const [id, uname, uemail, urole, createdAt] = result[0].values[0];
307
+ return { id, username: uname, email: uemail, role: urole, createdAt };
308
+ }
309
+ return null;
310
+ }
311
+
312
+ /**
313
+ * 根据用户名查找用户
314
+ */
315
+ export async function findUserByUsername(registryDir, username) {
316
+ await initDatabase(registryDir);
317
+ const result = db.exec(
318
+ `SELECT id, username, password_hash, email, role, created_at FROM users WHERE username = ?`,
319
+ [username]
320
+ );
321
+ if (result.length > 0 && result[0].values.length > 0) {
322
+ const [id, uname, passwordHash, uemail, urole, createdAt] = result[0].values[0];
323
+ return { id, username: uname, passwordHash, email: uemail, role: urole, createdAt };
324
+ }
325
+ return null;
326
+ }
327
+
328
+ /**
329
+ * 根据 ID 查找用户
330
+ */
331
+ export async function findUserById(registryDir, userId) {
332
+ await initDatabase(registryDir);
333
+ const result = db.exec(
334
+ `SELECT id, username, email, role, created_at FROM users WHERE id = ?`,
335
+ [userId]
336
+ );
337
+ if (result.length > 0 && result[0].values.length > 0) {
338
+ const [id, uname, uemail, urole, createdAt] = result[0].values[0];
339
+ return { id, username: uname, email: uemail, role: urole, createdAt };
340
+ }
341
+ return null;
342
+ }
343
+
344
+ // === P1: Token 管理 ===
345
+
346
+ /**
347
+ * 保存 API Token
348
+ */
349
+ export async function saveApiToken(registryDir, { userId, tokenHash, label, scopes, expiresAt }) {
350
+ await initDatabase(registryDir);
351
+ db.run(
352
+ `INSERT INTO api_tokens (user_id, token_hash, label, scopes, expires_at) VALUES (?, ?, ?, ?, ?)`,
353
+ [userId, tokenHash, label || "default", scopes || "read:agent,publish", expiresAt || null]
354
+ );
355
+ await saveDatabase();
356
+ }
357
+
358
+ /**
359
+ * 根据 Token 哈希查找 Token 记录
360
+ */
361
+ export async function findTokenByHash(registryDir, tokenHash) {
362
+ await initDatabase(registryDir);
363
+ const result = db.exec(
364
+ `SELECT t.id, t.user_id, t.scopes, t.expires_at, t.revoked_at, u.username, u.role
365
+ FROM api_tokens t JOIN users u ON t.user_id = u.id
366
+ WHERE t.token_hash = ?`,
367
+ [tokenHash]
368
+ );
369
+ if (result.length > 0 && result[0].values.length > 0) {
370
+ const [id, userId, scopes, expiresAt, revokedAt, username, role] = result[0].values[0];
371
+ // 检查是否已吊销或过期
372
+ if (revokedAt) return null;
373
+ if (expiresAt && new Date(expiresAt) < new Date()) return null;
374
+ return { id, userId, scopes, username, role };
375
+ }
376
+ return null;
377
+ }
378
+
379
+ /**
380
+ * 吊销 Token
381
+ */
382
+ export async function revokeToken(registryDir, tokenId) {
383
+ await initDatabase(registryDir);
384
+ db.run(`UPDATE api_tokens SET revoked_at = CURRENT_TIMESTAMP WHERE id = ?`, [tokenId]);
385
+ await saveDatabase();
386
+ }
387
+
388
+ /**
389
+ * 列出用户的所有 Token
390
+ */
391
+ export async function listUserTokens(registryDir, userId) {
392
+ await initDatabase(registryDir);
393
+ const result = db.exec(
394
+ `SELECT id, label, scopes, created_at, expires_at, revoked_at FROM api_tokens WHERE user_id = ? ORDER BY created_at DESC`,
395
+ [userId]
396
+ );
397
+ if (result.length > 0) {
398
+ return result[0].values.map(([id, label, scopes, createdAt, expiresAt, revokedAt]) => ({
399
+ id, label, scopes, createdAt, expiresAt, revokedAt,
400
+ active: !revokedAt && (!expiresAt || new Date(expiresAt) > new Date()),
401
+ }));
402
+ }
403
+ return [];
404
+ }
405
+
406
+ // === P1: 审计日志 ===
407
+
408
+ /**
409
+ * 记录审计日志
410
+ */
411
+ export async function addAuditLog(registryDir, { userId, username, action, resource, details, ipAddress }) {
412
+ await initDatabase(registryDir);
413
+ db.run(
414
+ `INSERT INTO audit_logs (user_id, username, action, resource, details, ip_address) VALUES (?, ?, ?, ?, ?, ?)`,
415
+ [userId || null, username || null, action, resource || null, typeof details === "object" ? JSON.stringify(details) : details || null, ipAddress || null]
416
+ );
417
+ await saveDatabase();
418
+ }
419
+
420
+ /**
421
+ * 查询审计日志
422
+ */
423
+ export async function queryAuditLogs(registryDir, { action, username, limit = 50 } = {}) {
424
+ await initDatabase(registryDir);
425
+ let sql = `SELECT id, user_id, username, action, resource, details, ip_address, created_at FROM audit_logs`;
426
+ const params = [];
427
+ const conditions = [];
428
+
429
+ if (action) {
430
+ conditions.push(`action = ?`);
431
+ params.push(action);
432
+ }
433
+ if (username) {
434
+ conditions.push(`username = ?`);
435
+ params.push(username);
436
+ }
437
+ if (conditions.length > 0) {
438
+ sql += ` WHERE ${conditions.join(" AND ")}`;
439
+ }
440
+ sql += ` ORDER BY created_at DESC LIMIT ?`;
441
+ params.push(limit);
442
+
443
+ const result = db.exec(sql, params);
444
+ if (result.length > 0) {
445
+ return result[0].values.map(([id, userId, uname, act, resource, details, ip, createdAt]) => ({
446
+ id, userId, username: uname, action: act, resource, details, ipAddress: ip, createdAt,
447
+ }));
448
+ }
449
+ return [];
450
+ }