@42ailab/42plugin 0.1.17 → 0.1.19
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/package.json +1 -1
- package/src/config.ts +4 -3
- package/src/db.ts +166 -15
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// 数据目录(支持环境变量覆盖,用于测试)
|
|
9
9
|
const dataDir =
|
|
10
|
-
process.
|
|
10
|
+
process.env.PLUGIN_DATA_DIR ||
|
|
11
|
+
(process.platform === 'win32'
|
|
11
12
|
? path.join(process.env.APPDATA || os.homedir(), '42plugin')
|
|
12
|
-
: path.join(os.homedir(), '.42plugin');
|
|
13
|
+
: path.join(os.homedir(), '.42plugin'));
|
|
13
14
|
|
|
14
15
|
export const config = {
|
|
15
16
|
// 目录
|
package/src/db.ts
CHANGED
|
@@ -24,9 +24,53 @@ const PROGRESS_THRESHOLD = 1024 * 1024; // 1MB
|
|
|
24
24
|
|
|
25
25
|
let db: Database | null = null;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 检查目录权限,返回友好的错误信息
|
|
29
|
+
*/
|
|
30
|
+
async function checkDirectoryPermissions(dir: string): Promise<void> {
|
|
31
|
+
try {
|
|
32
|
+
// 检查目录是否存在
|
|
33
|
+
const stat = await fs.stat(dir);
|
|
34
|
+
|
|
35
|
+
// 检查是否可写
|
|
36
|
+
await fs.access(dir, fs.constants.W_OK);
|
|
37
|
+
|
|
38
|
+
// 检查所有者(仅在非 Windows 系统)
|
|
39
|
+
if (process.platform !== 'win32') {
|
|
40
|
+
const expectedUid = process.getuid?.();
|
|
41
|
+
if (expectedUid !== undefined && stat.uid !== expectedUid && stat.uid === 0) {
|
|
42
|
+
throw new Error('OWNED_BY_ROOT');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
47
|
+
// 目录不存在,稍后会创建
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ((error as Error).message === 'OWNED_BY_ROOT' ||
|
|
52
|
+
(error as NodeJS.ErrnoException).code === 'EACCES') {
|
|
53
|
+
const homeDir = os.homedir();
|
|
54
|
+
throw new Error(
|
|
55
|
+
`数据目录权限错误: ${dir}\n\n` +
|
|
56
|
+
`这通常是因为之前使用了 sudo 运行 CLI 导致的。\n` +
|
|
57
|
+
`请执行以下命令修复权限:\n\n` +
|
|
58
|
+
` sudo chown -R $(whoami) ${path.join(homeDir, '.42plugin')}\n`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
27
66
|
async function getDb(): Promise<Database> {
|
|
28
67
|
if (!db) {
|
|
29
|
-
|
|
68
|
+
const dir = path.dirname(config.dbPath);
|
|
69
|
+
|
|
70
|
+
// 检查权限
|
|
71
|
+
await checkDirectoryPermissions(dir);
|
|
72
|
+
|
|
73
|
+
await fs.mkdir(dir, { recursive: true });
|
|
30
74
|
db = new Database(config.dbPath);
|
|
31
75
|
initSchema();
|
|
32
76
|
}
|
|
@@ -34,6 +78,9 @@ async function getDb(): Promise<Database> {
|
|
|
34
78
|
}
|
|
35
79
|
|
|
36
80
|
function initSchema(): void {
|
|
81
|
+
// 检查并执行数据库迁移
|
|
82
|
+
migrateIfNeeded();
|
|
83
|
+
|
|
37
84
|
db!.run(`
|
|
38
85
|
CREATE TABLE IF NOT EXISTS projects (
|
|
39
86
|
id TEXT PRIMARY KEY,
|
|
@@ -75,6 +122,82 @@ function initSchema(): void {
|
|
|
75
122
|
`);
|
|
76
123
|
}
|
|
77
124
|
|
|
125
|
+
/**
|
|
126
|
+
* 数据库迁移:处理旧版本 schema 升级
|
|
127
|
+
*/
|
|
128
|
+
function migrateIfNeeded(): void {
|
|
129
|
+
try {
|
|
130
|
+
// 检查 plugin_cache 表是否存在
|
|
131
|
+
const tableExists = db!.prepare(`
|
|
132
|
+
SELECT name FROM sqlite_master
|
|
133
|
+
WHERE type='table' AND name='plugin_cache'
|
|
134
|
+
`).get();
|
|
135
|
+
|
|
136
|
+
if (!tableExists) {
|
|
137
|
+
// 表不存在,无需迁移
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 获取表结构
|
|
142
|
+
const columns = db!.prepare('PRAGMA table_info(plugin_cache)').all() as { name: string }[];
|
|
143
|
+
const columnNames = columns.map(c => c.name);
|
|
144
|
+
|
|
145
|
+
// 检查是否是旧版本 schema(有 downloaded_at 但没有 cached_at)
|
|
146
|
+
const hasDownloadedAt = columnNames.includes('downloaded_at');
|
|
147
|
+
const hasCachedAt = columnNames.includes('cached_at');
|
|
148
|
+
|
|
149
|
+
if (hasDownloadedAt && !hasCachedAt) {
|
|
150
|
+
console.log('检测到旧版本数据库,正在迁移...');
|
|
151
|
+
|
|
152
|
+
// 开始事务
|
|
153
|
+
db!.run('BEGIN TRANSACTION');
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// 1. 重命名旧表
|
|
157
|
+
db!.run('ALTER TABLE plugin_cache RENAME TO plugin_cache_old');
|
|
158
|
+
|
|
159
|
+
// 2. 创建新表
|
|
160
|
+
db!.run(`
|
|
161
|
+
CREATE TABLE plugin_cache (
|
|
162
|
+
full_name TEXT NOT NULL,
|
|
163
|
+
version TEXT NOT NULL,
|
|
164
|
+
type TEXT NOT NULL,
|
|
165
|
+
cache_path TEXT NOT NULL,
|
|
166
|
+
checksum TEXT NOT NULL,
|
|
167
|
+
size_bytes INTEGER NOT NULL,
|
|
168
|
+
cached_at TEXT NOT NULL,
|
|
169
|
+
PRIMARY KEY (full_name, version)
|
|
170
|
+
)
|
|
171
|
+
`);
|
|
172
|
+
|
|
173
|
+
// 3. 迁移数据(downloaded_at → cached_at)
|
|
174
|
+
db!.run(`
|
|
175
|
+
INSERT INTO plugin_cache (full_name, version, type, cache_path, checksum, size_bytes, cached_at)
|
|
176
|
+
SELECT full_name, version, type, cache_path, checksum, COALESCE(size_bytes, 0), downloaded_at
|
|
177
|
+
FROM plugin_cache_old
|
|
178
|
+
`);
|
|
179
|
+
|
|
180
|
+
// 4. 删除旧表
|
|
181
|
+
db!.run('DROP TABLE plugin_cache_old');
|
|
182
|
+
|
|
183
|
+
// 提交事务
|
|
184
|
+
db!.run('COMMIT');
|
|
185
|
+
|
|
186
|
+
console.log('数据库迁移完成');
|
|
187
|
+
} catch (error) {
|
|
188
|
+
// 回滚事务
|
|
189
|
+
db!.run('ROLLBACK');
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// 迁移失败不应阻止程序运行,只记录警告
|
|
195
|
+
if (config.debug) {
|
|
196
|
+
console.warn('数据库迁移检查失败:', error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
78
201
|
// ============================================================================
|
|
79
202
|
// 项目管理
|
|
80
203
|
// ============================================================================
|
|
@@ -295,15 +418,27 @@ interface Secrets {
|
|
|
295
418
|
|
|
296
419
|
async function readSecrets(): Promise<Secrets> {
|
|
297
420
|
try {
|
|
421
|
+
// 先检查目录权限
|
|
422
|
+
await checkDirectoryPermissions(path.dirname(SECRETS_FILE));
|
|
423
|
+
|
|
298
424
|
const content = await fs.readFile(SECRETS_FILE, 'utf-8');
|
|
299
425
|
return JSON.parse(content);
|
|
300
|
-
} catch {
|
|
426
|
+
} catch (error) {
|
|
427
|
+
// 权限错误会直接抛出友好提示
|
|
428
|
+
if ((error as Error).message.includes('数据目录权限错误')) {
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
301
431
|
return {};
|
|
302
432
|
}
|
|
303
433
|
}
|
|
304
434
|
|
|
305
435
|
async function writeSecrets(secrets: Secrets): Promise<void> {
|
|
306
|
-
|
|
436
|
+
const dir = path.dirname(SECRETS_FILE);
|
|
437
|
+
|
|
438
|
+
// 检查目录权限
|
|
439
|
+
await checkDirectoryPermissions(dir);
|
|
440
|
+
|
|
441
|
+
await fs.mkdir(dir, { recursive: true });
|
|
307
442
|
await fs.writeFile(SECRETS_FILE, JSON.stringify(secrets, null, 2), { mode: 0o600 });
|
|
308
443
|
}
|
|
309
444
|
|
|
@@ -332,7 +467,8 @@ export async function downloadAndExtract(
|
|
|
332
467
|
url: string,
|
|
333
468
|
expectedChecksum: string,
|
|
334
469
|
fullName: string,
|
|
335
|
-
version: string
|
|
470
|
+
version: string,
|
|
471
|
+
pluginType: string
|
|
336
472
|
): Promise<string> {
|
|
337
473
|
const parts = fullName.split('/');
|
|
338
474
|
const targetDir = path.join(config.cacheDir, ...parts, version);
|
|
@@ -415,7 +551,7 @@ export async function downloadAndExtract(
|
|
|
415
551
|
await fs.unlink(tempFile).catch(() => {});
|
|
416
552
|
|
|
417
553
|
// 返回最终路径(如果只有一个子目录则进入)
|
|
418
|
-
return resolveFinalPath(targetDir);
|
|
554
|
+
return resolveFinalPath(targetDir, pluginType);
|
|
419
555
|
} catch (error) {
|
|
420
556
|
await fs.rm(targetDir, { recursive: true, force: true }).catch(() => {});
|
|
421
557
|
throw error;
|
|
@@ -424,8 +560,9 @@ export async function downloadAndExtract(
|
|
|
424
560
|
|
|
425
561
|
/**
|
|
426
562
|
* 修正旧缓存路径(如果是目录但应该是文件)
|
|
563
|
+
* @param pluginType 插件类型:skill 需要目录,agent/command 需要文件
|
|
427
564
|
*/
|
|
428
|
-
async function correctCachePath(cachePath: string): Promise<string> {
|
|
565
|
+
async function correctCachePath(cachePath: string, pluginType: string): Promise<string> {
|
|
429
566
|
try {
|
|
430
567
|
const stat = await fs.stat(cachePath);
|
|
431
568
|
if (!stat.isDirectory()) {
|
|
@@ -442,11 +579,16 @@ async function correctCachePath(cachePath: string): Promise<string> {
|
|
|
442
579
|
// 如果只有一个子目录(且没有其他文件),递归进入
|
|
443
580
|
if (dirs.length === 1 && nonMetadataFiles.length === 0) {
|
|
444
581
|
const subDir = path.join(cachePath, dirs[0].name);
|
|
445
|
-
return correctCachePath(subDir);
|
|
582
|
+
return correctCachePath(subDir, pluginType);
|
|
446
583
|
}
|
|
447
584
|
|
|
448
|
-
//
|
|
585
|
+
// Skill 类型:保持目录结构(包含 SKILL.md)
|
|
586
|
+
// Agent/Command 类型:返回 .md 文件路径
|
|
449
587
|
if (nonMetadataFiles.length === 1 && dirs.length === 0) {
|
|
588
|
+
if (pluginType === 'skill') {
|
|
589
|
+
// Skill 需要目录,不返回文件
|
|
590
|
+
return cachePath;
|
|
591
|
+
}
|
|
450
592
|
return path.join(cachePath, nonMetadataFiles[0].name);
|
|
451
593
|
}
|
|
452
594
|
|
|
@@ -456,7 +598,11 @@ async function correctCachePath(cachePath: string): Promise<string> {
|
|
|
456
598
|
}
|
|
457
599
|
}
|
|
458
600
|
|
|
459
|
-
|
|
601
|
+
/**
|
|
602
|
+
* 解析最终路径
|
|
603
|
+
* @param pluginType 插件类型:skill 需要目录,agent/command 需要文件
|
|
604
|
+
*/
|
|
605
|
+
async function resolveFinalPath(dir: string, pluginType: string): Promise<string> {
|
|
460
606
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
461
607
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith('.'));
|
|
462
608
|
const files = entries.filter((e) => e.isFile() && !e.name.startsWith('.'));
|
|
@@ -468,13 +614,17 @@ async function resolveFinalPath(dir: string): Promise<string> {
|
|
|
468
614
|
const subEntries = await fs.readdir(subDir);
|
|
469
615
|
if (subEntries.length > 0) {
|
|
470
616
|
// 递归查找,处理 agent/command 类型的 name/name.md 结构
|
|
471
|
-
return resolveFinalPath(subDir);
|
|
617
|
+
return resolveFinalPath(subDir, pluginType);
|
|
472
618
|
}
|
|
473
619
|
}
|
|
474
620
|
|
|
475
|
-
//
|
|
476
|
-
//
|
|
621
|
+
// Skill 类型:保持目录结构(包含 SKILL.md)
|
|
622
|
+
// Agent/Command 类型:返回 .md 文件路径
|
|
477
623
|
if (nonMetadataFiles.length === 1 && dirs.length === 0) {
|
|
624
|
+
if (pluginType === 'skill') {
|
|
625
|
+
// Skill 需要目录,不返回文件
|
|
626
|
+
return dir;
|
|
627
|
+
}
|
|
478
628
|
return path.join(dir, nonMetadataFiles[0].name);
|
|
479
629
|
}
|
|
480
630
|
|
|
@@ -566,8 +716,8 @@ export async function resolveCachePath(
|
|
|
566
716
|
// 验证缓存文件是否仍然存在
|
|
567
717
|
try {
|
|
568
718
|
await fs.access(cached.cachePath);
|
|
569
|
-
//
|
|
570
|
-
const correctedPath = await correctCachePath(cached.cachePath);
|
|
719
|
+
// 修正旧缓存的路径(根据类型:skill 需要目录,agent/command 需要文件)
|
|
720
|
+
const correctedPath = await correctCachePath(cached.cachePath, downloadInfo.type);
|
|
571
721
|
// 如果路径被修正,更新缓存记录
|
|
572
722
|
if (correctedPath !== cached.cachePath) {
|
|
573
723
|
await setCache({
|
|
@@ -591,7 +741,8 @@ export async function resolveCachePath(
|
|
|
591
741
|
downloadInfo.downloadUrl,
|
|
592
742
|
downloadInfo.checksum,
|
|
593
743
|
downloadInfo.fullName,
|
|
594
|
-
downloadInfo.version
|
|
744
|
+
downloadInfo.version,
|
|
745
|
+
downloadInfo.type
|
|
595
746
|
);
|
|
596
747
|
|
|
597
748
|
// 更新缓存记录
|