@spaceflow/core 0.1.1
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/CHANGELOG.md +1176 -0
- package/README.md +105 -0
- package/nest-cli.json +10 -0
- package/package.json +128 -0
- package/rspack.config.mjs +62 -0
- package/src/__mocks__/@opencode-ai/sdk.js +9 -0
- package/src/__mocks__/c12.ts +3 -0
- package/src/app.module.ts +18 -0
- package/src/config/ci.config.ts +29 -0
- package/src/config/config-loader.ts +101 -0
- package/src/config/config-reader.module.ts +16 -0
- package/src/config/config-reader.service.ts +133 -0
- package/src/config/feishu.config.ts +35 -0
- package/src/config/git-provider.config.ts +29 -0
- package/src/config/index.ts +29 -0
- package/src/config/llm.config.ts +110 -0
- package/src/config/schema-generator.service.ts +129 -0
- package/src/config/spaceflow.config.ts +292 -0
- package/src/config/storage.config.ts +33 -0
- package/src/extension-system/extension.interface.ts +221 -0
- package/src/extension-system/index.ts +1 -0
- package/src/index.ts +80 -0
- package/src/locales/en/translation.json +11 -0
- package/src/locales/zh-cn/translation.json +11 -0
- package/src/shared/claude-setup/claude-setup.module.ts +8 -0
- package/src/shared/claude-setup/claude-setup.service.ts +131 -0
- package/src/shared/claude-setup/index.ts +2 -0
- package/src/shared/editor-config/index.ts +23 -0
- package/src/shared/feishu-sdk/feishu-sdk.module.ts +77 -0
- package/src/shared/feishu-sdk/feishu-sdk.service.ts +130 -0
- package/src/shared/feishu-sdk/fieshu-card.service.ts +139 -0
- package/src/shared/feishu-sdk/index.ts +4 -0
- package/src/shared/feishu-sdk/types/card-action.ts +132 -0
- package/src/shared/feishu-sdk/types/card.ts +64 -0
- package/src/shared/feishu-sdk/types/common.ts +22 -0
- package/src/shared/feishu-sdk/types/index.ts +46 -0
- package/src/shared/feishu-sdk/types/message.ts +35 -0
- package/src/shared/feishu-sdk/types/module.ts +21 -0
- package/src/shared/feishu-sdk/types/user.ts +77 -0
- package/src/shared/git-provider/adapters/gitea.adapter.spec.ts +473 -0
- package/src/shared/git-provider/adapters/gitea.adapter.ts +499 -0
- package/src/shared/git-provider/adapters/github.adapter.spec.ts +341 -0
- package/src/shared/git-provider/adapters/github.adapter.ts +830 -0
- package/src/shared/git-provider/adapters/gitlab.adapter.ts +839 -0
- package/src/shared/git-provider/adapters/index.ts +3 -0
- package/src/shared/git-provider/detect-provider.spec.ts +195 -0
- package/src/shared/git-provider/detect-provider.ts +112 -0
- package/src/shared/git-provider/git-provider.interface.ts +188 -0
- package/src/shared/git-provider/git-provider.module.ts +73 -0
- package/src/shared/git-provider/git-provider.service.spec.ts +282 -0
- package/src/shared/git-provider/git-provider.service.ts +309 -0
- package/src/shared/git-provider/index.ts +7 -0
- package/src/shared/git-provider/parse-repo-url.spec.ts +221 -0
- package/src/shared/git-provider/parse-repo-url.ts +155 -0
- package/src/shared/git-provider/types.ts +434 -0
- package/src/shared/git-sdk/git-sdk-diff.utils.spec.ts +344 -0
- package/src/shared/git-sdk/git-sdk-diff.utils.ts +151 -0
- package/src/shared/git-sdk/git-sdk.module.ts +8 -0
- package/src/shared/git-sdk/git-sdk.service.ts +235 -0
- package/src/shared/git-sdk/git-sdk.types.ts +25 -0
- package/src/shared/git-sdk/index.ts +4 -0
- package/src/shared/i18n/i18n.spec.ts +96 -0
- package/src/shared/i18n/i18n.ts +86 -0
- package/src/shared/i18n/index.ts +1 -0
- package/src/shared/i18n/locale-detect.ts +134 -0
- package/src/shared/llm-jsonput/index.ts +94 -0
- package/src/shared/llm-jsonput/types.ts +17 -0
- package/src/shared/llm-proxy/adapters/claude-code.adapter.spec.ts +131 -0
- package/src/shared/llm-proxy/adapters/claude-code.adapter.ts +208 -0
- package/src/shared/llm-proxy/adapters/index.ts +4 -0
- package/src/shared/llm-proxy/adapters/llm-adapter.interface.ts +23 -0
- package/src/shared/llm-proxy/adapters/open-code.adapter.ts +342 -0
- package/src/shared/llm-proxy/adapters/openai.adapter.spec.ts +215 -0
- package/src/shared/llm-proxy/adapters/openai.adapter.ts +153 -0
- package/src/shared/llm-proxy/index.ts +6 -0
- package/src/shared/llm-proxy/interfaces/config.interface.ts +32 -0
- package/src/shared/llm-proxy/interfaces/index.ts +4 -0
- package/src/shared/llm-proxy/interfaces/message.interface.ts +48 -0
- package/src/shared/llm-proxy/interfaces/session.interface.ts +28 -0
- package/src/shared/llm-proxy/llm-proxy.module.ts +140 -0
- package/src/shared/llm-proxy/llm-proxy.service.spec.ts +303 -0
- package/src/shared/llm-proxy/llm-proxy.service.ts +132 -0
- package/src/shared/llm-proxy/llm-session.spec.ts +111 -0
- package/src/shared/llm-proxy/llm-session.ts +109 -0
- package/src/shared/llm-proxy/stream-logger.ts +97 -0
- package/src/shared/logger/index.ts +11 -0
- package/src/shared/logger/logger.interface.ts +93 -0
- package/src/shared/logger/logger.spec.ts +178 -0
- package/src/shared/logger/logger.ts +175 -0
- package/src/shared/logger/renderers/plain.renderer.ts +116 -0
- package/src/shared/logger/renderers/tui.renderer.ts +162 -0
- package/src/shared/mcp/index.ts +332 -0
- package/src/shared/output/index.ts +2 -0
- package/src/shared/output/output.module.ts +9 -0
- package/src/shared/output/output.service.ts +97 -0
- package/src/shared/package-manager/index.ts +115 -0
- package/src/shared/parallel/index.ts +1 -0
- package/src/shared/parallel/parallel-executor.ts +169 -0
- package/src/shared/rspack-config/index.ts +1 -0
- package/src/shared/rspack-config/rspack-config.ts +157 -0
- package/src/shared/source-utils/index.ts +130 -0
- package/src/shared/spaceflow-dir/index.ts +158 -0
- package/src/shared/storage/adapters/file.adapter.ts +113 -0
- package/src/shared/storage/adapters/index.ts +3 -0
- package/src/shared/storage/adapters/memory.adapter.ts +50 -0
- package/src/shared/storage/adapters/storage-adapter.interface.ts +48 -0
- package/src/shared/storage/index.ts +4 -0
- package/src/shared/storage/storage.module.ts +150 -0
- package/src/shared/storage/storage.service.ts +293 -0
- package/src/shared/storage/types.ts +51 -0
- package/src/shared/verbose/index.ts +73 -0
- package/test/app.e2e-spec.ts +22 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +25 -0
- package/tsconfig.skill.json +18 -0
- package/vitest.config.ts +58 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { SPACEFLOW_DIR, PACKAGE_JSON } from "../../extension-system/extension.interface";
|
|
5
|
+
import { isPnpmWorkspace } from "../package-manager";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 获取 .spaceflow 目录路径
|
|
9
|
+
* @param isGlobal 是否为全局目录(~/.spaceflow)
|
|
10
|
+
* @param cwd 工作目录,默认为 process.cwd()
|
|
11
|
+
*/
|
|
12
|
+
export function getSpaceflowDir(isGlobal: boolean, cwd?: string): string {
|
|
13
|
+
if (isGlobal) {
|
|
14
|
+
return join(homedir(), SPACEFLOW_DIR);
|
|
15
|
+
}
|
|
16
|
+
return join(cwd || process.cwd(), SPACEFLOW_DIR);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 确保 .spaceflow 目录存在
|
|
21
|
+
* @param spaceflowDir .spaceflow 目录路径
|
|
22
|
+
*/
|
|
23
|
+
export function ensureSpaceflowDir(spaceflowDir: string): void {
|
|
24
|
+
if (!existsSync(spaceflowDir)) {
|
|
25
|
+
mkdirSync(spaceflowDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 创建 .gitignore
|
|
29
|
+
const gitignorePath = join(spaceflowDir, ".gitignore");
|
|
30
|
+
if (!existsSync(gitignorePath)) {
|
|
31
|
+
const gitignoreContent = `# Spaceflow Extension dependencies
|
|
32
|
+
node_modules/
|
|
33
|
+
pnpm-lock.yaml
|
|
34
|
+
config-schema.json
|
|
35
|
+
`;
|
|
36
|
+
writeFileSync(gitignorePath, gitignoreContent);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 创建空的 pnpm-workspace.yaml,防止被父级 workspace 接管
|
|
40
|
+
const workspaceYamlPath = join(spaceflowDir, "pnpm-workspace.yaml");
|
|
41
|
+
if (!existsSync(workspaceYamlPath)) {
|
|
42
|
+
writeFileSync(workspaceYamlPath, "packages: []\n");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 获取 @spaceflow/cli 的路径
|
|
48
|
+
* @param isGlobal 是否为全局安装
|
|
49
|
+
* @param cwd 工作目录,默认为 process.cwd()
|
|
50
|
+
*/
|
|
51
|
+
export function getSpaceflowCliPath(isGlobal: boolean = false, cwd?: string): string {
|
|
52
|
+
const workDir = cwd || process.cwd();
|
|
53
|
+
|
|
54
|
+
if (isGlobal) {
|
|
55
|
+
// 全局安装:尝试找到 @spaceflow/cli 的实际路径进行 link
|
|
56
|
+
// 优先从当前项目的 cli 目录 link
|
|
57
|
+
const cliPath = join(workDir, "packages", "cli");
|
|
58
|
+
if (existsSync(join(cliPath, PACKAGE_JSON))) {
|
|
59
|
+
try {
|
|
60
|
+
const content = readFileSync(join(cliPath, PACKAGE_JSON), "utf-8");
|
|
61
|
+
const pkg = JSON.parse(content);
|
|
62
|
+
if (pkg.name === "@spaceflow/cli") {
|
|
63
|
+
return `link:${cliPath}`;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// ignore
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// 回退到 latest
|
|
70
|
+
return "latest";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 本地安装:检查是否在 monorepo 中
|
|
74
|
+
if (isPnpmWorkspace(workDir)) {
|
|
75
|
+
return "workspace:*";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 尝试从项目 package.json 获取版本
|
|
79
|
+
const projectPkgPath = join(workDir, PACKAGE_JSON);
|
|
80
|
+
if (existsSync(projectPkgPath)) {
|
|
81
|
+
try {
|
|
82
|
+
const content = readFileSync(projectPkgPath, "utf-8");
|
|
83
|
+
const pkg = JSON.parse(content);
|
|
84
|
+
const version =
|
|
85
|
+
pkg.dependencies?.["@spaceflow/core"] || pkg.devDependencies?.["@spaceflow/core"];
|
|
86
|
+
if (version) return version;
|
|
87
|
+
} catch {
|
|
88
|
+
// ignore
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return "latest";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 确保 .spaceflow/package.json 存在
|
|
97
|
+
* 包含 @spaceflow/cli 作为依赖
|
|
98
|
+
* @param spaceflowDir .spaceflow 目录路径
|
|
99
|
+
* @param isGlobal 是否为全局安装
|
|
100
|
+
* @param cwd 工作目录,默认为 process.cwd()
|
|
101
|
+
*/
|
|
102
|
+
export function ensureSpaceflowPackageJson(
|
|
103
|
+
spaceflowDir: string,
|
|
104
|
+
isGlobal: boolean = false,
|
|
105
|
+
cwd?: string,
|
|
106
|
+
): void {
|
|
107
|
+
// 确保目录存在
|
|
108
|
+
ensureSpaceflowDir(spaceflowDir);
|
|
109
|
+
|
|
110
|
+
// 确保 package.json 存在
|
|
111
|
+
const packageJsonPath = join(spaceflowDir, PACKAGE_JSON);
|
|
112
|
+
if (!existsSync(packageJsonPath)) {
|
|
113
|
+
const cliPath = getSpaceflowCliPath(isGlobal, cwd);
|
|
114
|
+
const packageJson = {
|
|
115
|
+
name: "spaceflow",
|
|
116
|
+
private: true,
|
|
117
|
+
dependencies: {
|
|
118
|
+
"@spaceflow/core": cliPath,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 确保编辑器目录有 .gitignore 文件,并将生成的文件加入忽略列表
|
|
127
|
+
* @param editorRoot 编辑器根目录(如 .claude)
|
|
128
|
+
* @param itemType 项目类型 (skills 或 commands)
|
|
129
|
+
* @param itemName 项目名称
|
|
130
|
+
*/
|
|
131
|
+
export async function ensureEditorGitignore(
|
|
132
|
+
editorRoot: string,
|
|
133
|
+
itemType: "skills" | "commands",
|
|
134
|
+
itemName: string,
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
const { readFile, writeFile } = await import("fs/promises");
|
|
137
|
+
const gitignorePath = join(editorRoot, ".gitignore");
|
|
138
|
+
const ignoreEntry = itemType === "skills" ? `skills/${itemName}` : `commands/${itemName}.md`;
|
|
139
|
+
|
|
140
|
+
let content = "";
|
|
141
|
+
if (existsSync(gitignorePath)) {
|
|
142
|
+
content = await readFile(gitignorePath, "utf-8");
|
|
143
|
+
// 检查是否已包含该条目
|
|
144
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
145
|
+
if (lines.includes(ignoreEntry)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// 确保末尾有换行
|
|
149
|
+
if (!content.endsWith("\n")) {
|
|
150
|
+
content += "\n";
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
content = "# 自动生成的 .gitignore - spaceflow 安装的 skills 和 commands\n";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
content += `${ignoreEntry}\n`;
|
|
157
|
+
await writeFile(gitignorePath, content);
|
|
158
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { StorageItem } from "../types";
|
|
4
|
+
import { StorageAdapter } from "./storage-adapter.interface";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 文件存储适配器
|
|
8
|
+
* 数据持久化到 JSON 文件,支持进程重启后恢复
|
|
9
|
+
*/
|
|
10
|
+
export class FileAdapter implements StorageAdapter {
|
|
11
|
+
protected store = new Map<string, StorageItem>();
|
|
12
|
+
protected filePath: string;
|
|
13
|
+
protected saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
14
|
+
protected readonly debounceMs = 100; // 防抖延迟
|
|
15
|
+
|
|
16
|
+
constructor(filePath: string) {
|
|
17
|
+
this.filePath = filePath;
|
|
18
|
+
this.load();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 从文件加载数据
|
|
23
|
+
*/
|
|
24
|
+
protected load(): void {
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync(this.filePath)) {
|
|
27
|
+
const data = fs.readFileSync(this.filePath, "utf-8");
|
|
28
|
+
const parsed = JSON.parse(data) as Record<string, StorageItem>;
|
|
29
|
+
this.store = new Map(Object.entries(parsed));
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// 文件不存在或解析失败,使用空存储
|
|
33
|
+
this.store = new Map();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 保存数据到文件(防抖)
|
|
39
|
+
*/
|
|
40
|
+
protected save(): void {
|
|
41
|
+
if (this.saveTimer) {
|
|
42
|
+
clearTimeout(this.saveTimer);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.saveTimer = setTimeout(() => {
|
|
46
|
+
this.saveSync();
|
|
47
|
+
this.saveTimer = null;
|
|
48
|
+
}, this.debounceMs);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 同步保存数据到文件
|
|
53
|
+
*/
|
|
54
|
+
protected saveSync(): void {
|
|
55
|
+
try {
|
|
56
|
+
const dir = path.dirname(this.filePath);
|
|
57
|
+
if (!fs.existsSync(dir)) {
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const data = Object.fromEntries(this.store);
|
|
62
|
+
fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error("Failed to save storage file:", error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async get<T = any>(key: string): Promise<StorageItem<T> | undefined> {
|
|
69
|
+
return this.store.get(key) as StorageItem<T> | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async set<T = any>(key: string, item: StorageItem<T>): Promise<void> {
|
|
73
|
+
this.store.set(key, item);
|
|
74
|
+
this.save();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async delete(key: string): Promise<boolean> {
|
|
78
|
+
const result = this.store.delete(key);
|
|
79
|
+
if (result) {
|
|
80
|
+
this.save();
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async has(key: string): Promise<boolean> {
|
|
86
|
+
return this.store.has(key);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async keys(pattern?: string): Promise<string[]> {
|
|
90
|
+
const allKeys = Array.from(this.store.keys());
|
|
91
|
+
|
|
92
|
+
if (!pattern) {
|
|
93
|
+
return allKeys;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 将通配符模式转换为正则表达式
|
|
97
|
+
const regexPattern = pattern
|
|
98
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // 转义特殊字符
|
|
99
|
+
.replace(/\*/g, ".*"); // * 转换为 .*
|
|
100
|
+
|
|
101
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
102
|
+
return allKeys.filter((key) => regex.test(key));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async clear(): Promise<void> {
|
|
106
|
+
this.store.clear();
|
|
107
|
+
this.save();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async size(): Promise<number> {
|
|
111
|
+
return this.store.size;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { StorageItem } from "../types";
|
|
2
|
+
import { StorageAdapter } from "./storage-adapter.interface";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 内存存储适配器
|
|
6
|
+
* 数据存储在内存中,进程重启后数据丢失
|
|
7
|
+
*/
|
|
8
|
+
export class MemoryAdapter implements StorageAdapter {
|
|
9
|
+
protected store = new Map<string, StorageItem>();
|
|
10
|
+
|
|
11
|
+
async get<T = any>(key: string): Promise<StorageItem<T> | undefined> {
|
|
12
|
+
return this.store.get(key) as StorageItem<T> | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async set<T = any>(key: string, item: StorageItem<T>): Promise<void> {
|
|
16
|
+
this.store.set(key, item);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async delete(key: string): Promise<boolean> {
|
|
20
|
+
return this.store.delete(key);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async has(key: string): Promise<boolean> {
|
|
24
|
+
return this.store.has(key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async keys(pattern?: string): Promise<string[]> {
|
|
28
|
+
const allKeys = Array.from(this.store.keys());
|
|
29
|
+
|
|
30
|
+
if (!pattern) {
|
|
31
|
+
return allKeys;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 将通配符模式转换为正则表达式
|
|
35
|
+
const regexPattern = pattern
|
|
36
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // 转义特殊字符
|
|
37
|
+
.replace(/\*/g, ".*"); // * 转换为 .*
|
|
38
|
+
|
|
39
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
40
|
+
return allKeys.filter((key) => regex.test(key));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async clear(): Promise<void> {
|
|
44
|
+
this.store.clear();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async size(): Promise<number> {
|
|
48
|
+
return this.store.size;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { StorageItem } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Storage 适配器接口
|
|
5
|
+
* 所有适配器必须实现此接口
|
|
6
|
+
*/
|
|
7
|
+
export interface StorageAdapter {
|
|
8
|
+
/**
|
|
9
|
+
* 获取存储项
|
|
10
|
+
* @param key 键名
|
|
11
|
+
*/
|
|
12
|
+
get<T = any>(key: string): Promise<StorageItem<T> | undefined>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 设置存储项
|
|
16
|
+
* @param key 键名
|
|
17
|
+
* @param item 存储项
|
|
18
|
+
*/
|
|
19
|
+
set<T = any>(key: string, item: StorageItem<T>): Promise<void>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 删除存储项
|
|
23
|
+
* @param key 键名
|
|
24
|
+
*/
|
|
25
|
+
delete(key: string): Promise<boolean>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 检查键是否存在
|
|
29
|
+
* @param key 键名
|
|
30
|
+
*/
|
|
31
|
+
has(key: string): Promise<boolean>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 获取所有键名
|
|
35
|
+
* @param pattern 可选的匹配模式,支持 * 通配符
|
|
36
|
+
*/
|
|
37
|
+
keys(pattern?: string): Promise<string[]>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 清空所有存储
|
|
41
|
+
*/
|
|
42
|
+
clear(): Promise<void>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 获取当前存储的 key 数量
|
|
46
|
+
*/
|
|
47
|
+
size(): Promise<number>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { DynamicModule, Module } from "@nestjs/common";
|
|
2
|
+
import { ConfigModule, ConfigService } from "@nestjs/config";
|
|
3
|
+
import { StorageService } from "./storage.service";
|
|
4
|
+
import { MemoryAdapter } from "./adapters/memory.adapter";
|
|
5
|
+
import { FileAdapter } from "./adapters/file.adapter";
|
|
6
|
+
import { StorageAdapter } from "./adapters/storage-adapter.interface";
|
|
7
|
+
import {
|
|
8
|
+
type StorageModuleOptions,
|
|
9
|
+
type StorageModuleAsyncOptions,
|
|
10
|
+
STORAGE_MODULE_OPTIONS,
|
|
11
|
+
STORAGE_ADAPTER,
|
|
12
|
+
} from "./types";
|
|
13
|
+
import { storageConfig, StorageConfig } from "../../config";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 创建适配器实例
|
|
17
|
+
*/
|
|
18
|
+
function createAdapter(options: StorageModuleOptions): StorageAdapter {
|
|
19
|
+
switch (options.adapter) {
|
|
20
|
+
case "file":
|
|
21
|
+
if (!options.filePath) {
|
|
22
|
+
throw new Error("filePath is required for file adapter");
|
|
23
|
+
}
|
|
24
|
+
return new FileAdapter(options.filePath);
|
|
25
|
+
case "memory":
|
|
26
|
+
default:
|
|
27
|
+
return new MemoryAdapter();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Module({})
|
|
32
|
+
export class StorageModule {
|
|
33
|
+
/**
|
|
34
|
+
* 同步注册模块
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* StorageModule.forRoot({
|
|
39
|
+
* adapter: 'memory',
|
|
40
|
+
* defaultTtl: 3600000, // 1 hour
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* StorageModule.forRoot({
|
|
47
|
+
* adapter: 'file',
|
|
48
|
+
* filePath: './data/storage.json',
|
|
49
|
+
* })
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
static forRoot(options: StorageModuleOptions): DynamicModule {
|
|
53
|
+
return {
|
|
54
|
+
module: StorageModule,
|
|
55
|
+
providers: [
|
|
56
|
+
{
|
|
57
|
+
provide: STORAGE_MODULE_OPTIONS,
|
|
58
|
+
useValue: options,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
provide: STORAGE_ADAPTER,
|
|
62
|
+
useValue: createAdapter(options),
|
|
63
|
+
},
|
|
64
|
+
StorageService,
|
|
65
|
+
],
|
|
66
|
+
exports: [StorageService],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 异步注册模块
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* StorageModule.forRootAsync({
|
|
76
|
+
* useFactory: (configService: ConfigService) => ({
|
|
77
|
+
* adapter: configService.get('STORAGE_ADAPTER') || 'memory',
|
|
78
|
+
* filePath: configService.get('STORAGE_FILE_PATH'),
|
|
79
|
+
* }),
|
|
80
|
+
* inject: [ConfigService],
|
|
81
|
+
* })
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
static forRootAsync(options: StorageModuleAsyncOptions): DynamicModule {
|
|
85
|
+
return {
|
|
86
|
+
module: StorageModule,
|
|
87
|
+
providers: [
|
|
88
|
+
{
|
|
89
|
+
provide: STORAGE_MODULE_OPTIONS,
|
|
90
|
+
useFactory: options.useFactory,
|
|
91
|
+
inject: options.inject || [],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
provide: STORAGE_ADAPTER,
|
|
95
|
+
useFactory: (opts: StorageModuleOptions) => createAdapter(opts),
|
|
96
|
+
inject: [STORAGE_MODULE_OPTIONS],
|
|
97
|
+
},
|
|
98
|
+
StorageService,
|
|
99
|
+
],
|
|
100
|
+
exports: [StorageService],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 使用 ConfigService 注册模块
|
|
106
|
+
*
|
|
107
|
+
* 环境变量:
|
|
108
|
+
* - STORAGE_ADAPTER: 'memory' | 'file'
|
|
109
|
+
* - STORAGE_FILE_PATH: 文件存储路径
|
|
110
|
+
* - STORAGE_DEFAULT_TTL: 默认过期时间(毫秒)
|
|
111
|
+
* - STORAGE_MAX_KEYS: 最大 key 数量
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* @Module({
|
|
116
|
+
* imports: [StorageModule.forFeature()],
|
|
117
|
+
* })
|
|
118
|
+
* export class AppModule {}
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
static forFeature(): DynamicModule {
|
|
122
|
+
return {
|
|
123
|
+
global: true,
|
|
124
|
+
module: StorageModule,
|
|
125
|
+
imports: [ConfigModule.forFeature(storageConfig)],
|
|
126
|
+
providers: [
|
|
127
|
+
{
|
|
128
|
+
provide: STORAGE_MODULE_OPTIONS,
|
|
129
|
+
useFactory: (configService: ConfigService): StorageModuleOptions => {
|
|
130
|
+
const config = configService.get<StorageConfig>("storage");
|
|
131
|
+
return {
|
|
132
|
+
adapter: config?.adapter || "memory",
|
|
133
|
+
filePath: config?.filePath,
|
|
134
|
+
defaultTtl: config?.defaultTtl,
|
|
135
|
+
maxKeys: config?.maxKeys,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
inject: [ConfigService],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
provide: STORAGE_ADAPTER,
|
|
142
|
+
useFactory: (opts: StorageModuleOptions) => createAdapter(opts),
|
|
143
|
+
inject: [STORAGE_MODULE_OPTIONS],
|
|
144
|
+
},
|
|
145
|
+
StorageService,
|
|
146
|
+
],
|
|
147
|
+
exports: [StorageService],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|