ai-spec-dev 0.38.0 → 0.42.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/.ai-spec-workspace.json +17 -0
- package/.ai-spec.json +7 -0
- package/cli/commands/create.ts +9 -1176
- package/cli/commands/dashboard.ts +1 -1
- package/cli/pipeline/helpers.ts +34 -0
- package/cli/pipeline/multi-repo.ts +483 -0
- package/cli/pipeline/single-repo.ts +764 -0
- package/cli/utils.ts +2 -0
- package/core/cli-ui.ts +136 -0
- package/core/code-generator.ts +56 -343
- package/core/codegen/helpers.ts +219 -0
- package/core/codegen/topo-sort.ts +98 -0
- package/core/constitution-consolidator.ts +2 -2
- package/core/dsl-coverage-checker.ts +298 -0
- package/core/dsl-extractor.ts +19 -46
- package/core/dsl-feedback.ts +1 -1
- package/core/dsl-validator.ts +74 -0
- package/core/error-feedback.ts +99 -13
- package/core/frontend-context-loader.ts +27 -5
- package/core/knowledge-memory.ts +52 -0
- package/core/mock/fixtures.ts +89 -0
- package/core/mock/proxy.ts +380 -0
- package/core/mock-server-generator.ts +12 -460
- package/core/provider-utils.ts +8 -7
- package/core/requirement-decomposer.ts +4 -28
- package/core/reviewer.ts +1 -1
- package/core/safe-json.ts +76 -0
- package/core/spec-updater.ts +5 -21
- package/core/token-budget.ts +124 -0
- package/core/vcr.ts +20 -1
- package/demo-backend/.ai-spec-constitution.md +65 -0
- package/demo-backend/package.json +21 -0
- package/demo-backend/prisma/schema.prisma +22 -0
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +186 -0
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +211 -0
- package/demo-backend/src/controllers/bookmark.controller.test.ts +255 -0
- package/demo-backend/src/controllers/bookmark.controller.ts +187 -0
- package/demo-backend/src/index.ts +17 -0
- package/demo-backend/src/routes/bookmark.routes.test.ts +264 -0
- package/demo-backend/src/routes/bookmark.routes.ts +11 -0
- package/demo-backend/src/routes/index.ts +8 -0
- package/demo-backend/src/services/bookmark.service.test.ts +433 -0
- package/demo-backend/src/services/bookmark.service.ts +261 -0
- package/demo-backend/tsconfig.json +12 -0
- package/demo-frontend/.ai-spec-constitution.md +95 -0
- package/demo-frontend/package.json +23 -0
- package/demo-frontend/src/App.tsx +12 -0
- package/demo-frontend/src/main.tsx +9 -0
- package/demo-frontend/tsconfig.json +13 -0
- package/dist/cli/index.js +4351 -3666
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +3997 -3312
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +18 -16
- package/dist/index.d.ts +18 -16
- package/dist/index.js +388 -188
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +386 -186
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/tests/auto-consolidation.test.ts +109 -0
- package/tests/combined-generator.test.ts +81 -0
- package/tests/constitution-consolidator.test.ts +161 -0
- package/tests/constitution-generator.test.ts +94 -0
- package/tests/contract-bridge.test.ts +201 -0
- package/tests/design-dialogue.test.ts +108 -0
- package/tests/dsl-coverage-checker.test.ts +230 -0
- package/tests/dsl-feedback.test.ts +45 -0
- package/tests/dsl-validator-xref.test.ts +99 -0
- package/tests/error-feedback-repair.test.ts +319 -0
- package/tests/error-feedback-validation.test.ts +91 -0
- package/tests/frontend-context-loader.test.ts +609 -0
- package/tests/global-constitution.test.ts +110 -0
- package/tests/key-store.test.ts +73 -0
- package/tests/knowledge-memory.test.ts +327 -0
- package/tests/project-index.test.ts +206 -0
- package/tests/prompt-hasher.test.ts +19 -0
- package/tests/requirement-decomposer.test.ts +171 -0
- package/tests/reviewer.test.ts +4 -1
- package/tests/run-logger.test.ts +289 -0
- package/tests/run-snapshot.test.ts +113 -0
- package/tests/safe-json.test.ts +63 -0
- package/tests/spec-updater.test.ts +161 -0
- package/tests/test-generator.test.ts +146 -0
- package/tests/token-budget.test.ts +124 -0
- package/tests/vcr-hash.test.ts +101 -0
- package/tests/workspace-loader.test.ts +277 -0
- package/RELEASE_LOG.md +0 -2731
- package/purpose.md +0 -1294
package/core/spec-updater.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
buildAffectedFilesPrompt,
|
|
16
16
|
} from "../prompts/update.prompt";
|
|
17
17
|
import { getCodeGenSystemPrompt } from "../prompts/codegen.prompt";
|
|
18
|
+
import { parseJsonFromAiOutput } from "./safe-json";
|
|
18
19
|
|
|
19
20
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
20
21
|
|
|
@@ -44,27 +45,10 @@ export interface SpecUpdaterOptions {
|
|
|
44
45
|
repoType?: string;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
// ─── JSON
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (trimmed.startsWith("{")) return JSON.parse(trimmed);
|
|
52
|
-
const fenceStart = trimmed.indexOf("```");
|
|
53
|
-
if (fenceStart !== -1) {
|
|
54
|
-
const afterFence = trimmed.slice(fenceStart + 3);
|
|
55
|
-
const newlinePos = afterFence.indexOf("\n");
|
|
56
|
-
const jsonStart = newlinePos !== -1 ? newlinePos + 1 : 0;
|
|
57
|
-
const fenceEnd = afterFence.lastIndexOf("```");
|
|
58
|
-
if (fenceEnd > jsonStart) return JSON.parse(afterFence.slice(jsonStart, fenceEnd).trim());
|
|
59
|
-
}
|
|
60
|
-
const objStart = trimmed.indexOf("{");
|
|
61
|
-
const arrStart = trimmed.indexOf("[");
|
|
62
|
-
const start = objStart !== -1 && (arrStart === -1 || objStart < arrStart) ? objStart : arrStart;
|
|
63
|
-
const isObj = start === objStart && objStart !== -1;
|
|
64
|
-
const end = isObj ? trimmed.lastIndexOf("}") : trimmed.lastIndexOf("]");
|
|
65
|
-
if (start !== -1 && end > start) return JSON.parse(trimmed.slice(start, end + 1));
|
|
66
|
-
throw new SyntaxError("No JSON found in output");
|
|
67
|
-
}
|
|
48
|
+
// ─── JSON Parser ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
// Uses shared parseJsonFromAiOutput from safe-json.ts
|
|
51
|
+
const parseJsonFromOutput = parseJsonFromAiOutput;
|
|
68
52
|
|
|
69
53
|
function parseAffectedFiles(raw: string): AffectedFile[] {
|
|
70
54
|
try {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* token-budget.ts — Lightweight token estimation and priority-based context assembly.
|
|
3
|
+
*
|
|
4
|
+
* Prevents silent context window overflow by estimating tokens and
|
|
5
|
+
* trimming lower-priority sections when the budget is exceeded.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
|
|
10
|
+
// ─── Token Estimation ────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** CJK character range. */
|
|
13
|
+
const CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Estimate token count for a string.
|
|
17
|
+
* CJK characters ≈ 1 token each; English/code ≈ 1 token per 4 characters.
|
|
18
|
+
* This is deliberately conservative (over-estimates slightly) to avoid
|
|
19
|
+
* exceeding the actual context window.
|
|
20
|
+
*/
|
|
21
|
+
export function estimateTokens(text: string): number {
|
|
22
|
+
if (!text) return 0;
|
|
23
|
+
const cjkCount = (text.match(CJK_RANGE) ?? []).length;
|
|
24
|
+
const nonCjkLength = text.length - cjkCount;
|
|
25
|
+
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Budget Assembly ────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A named section of context with a priority level.
|
|
32
|
+
* Lower priority number = higher importance (trimmed last).
|
|
33
|
+
*/
|
|
34
|
+
export interface BudgetSection {
|
|
35
|
+
/** Section name for logging. */
|
|
36
|
+
name: string;
|
|
37
|
+
/** The actual text content. */
|
|
38
|
+
content: string;
|
|
39
|
+
/** Priority: 1 = highest (never trim), 5 = lowest (trim first). */
|
|
40
|
+
priority: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BudgetResult {
|
|
44
|
+
/** The assembled prompt text (all included sections concatenated). */
|
|
45
|
+
assembledPrompt: string;
|
|
46
|
+
/** Total estimated tokens in the assembled prompt. */
|
|
47
|
+
totalTokens: number;
|
|
48
|
+
/** Names of sections that were trimmed or dropped. */
|
|
49
|
+
trimmedSections: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Assemble context sections within a token budget.
|
|
54
|
+
*
|
|
55
|
+
* Sections are added in priority order (P1 first). When the budget is
|
|
56
|
+
* exceeded, lower-priority sections are truncated or dropped entirely.
|
|
57
|
+
*
|
|
58
|
+
* @param sections - Context sections to assemble.
|
|
59
|
+
* @param maxTokens - Maximum token budget.
|
|
60
|
+
*/
|
|
61
|
+
export function assembleSections(
|
|
62
|
+
sections: BudgetSection[],
|
|
63
|
+
maxTokens: number
|
|
64
|
+
): BudgetResult {
|
|
65
|
+
// Sort by priority (ascending = most important first)
|
|
66
|
+
const sorted = [...sections].sort((a, b) => a.priority - b.priority);
|
|
67
|
+
|
|
68
|
+
const included: string[] = [];
|
|
69
|
+
const trimmedSections: string[] = [];
|
|
70
|
+
let usedTokens = 0;
|
|
71
|
+
|
|
72
|
+
for (const section of sorted) {
|
|
73
|
+
if (!section.content) continue;
|
|
74
|
+
|
|
75
|
+
const sectionTokens = estimateTokens(section.content);
|
|
76
|
+
|
|
77
|
+
if (usedTokens + sectionTokens <= maxTokens) {
|
|
78
|
+
// Fits entirely
|
|
79
|
+
included.push(section.content);
|
|
80
|
+
usedTokens += sectionTokens;
|
|
81
|
+
} else {
|
|
82
|
+
const remainingBudget = maxTokens - usedTokens;
|
|
83
|
+
if (remainingBudget <= 100) {
|
|
84
|
+
// Not enough room even for a truncated version
|
|
85
|
+
trimmedSections.push(section.name);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Truncate to fit remaining budget (approximate: 4 chars per token for safety)
|
|
90
|
+
const charBudget = Math.floor(remainingBudget * 3);
|
|
91
|
+
const truncated = section.content.slice(0, charBudget);
|
|
92
|
+
included.push(truncated + `\n\n... [${section.name} truncated — context budget reached]`);
|
|
93
|
+
trimmedSections.push(section.name);
|
|
94
|
+
usedTokens = maxTokens; // budget exhausted
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const assembledPrompt = included.join("\n\n");
|
|
99
|
+
|
|
100
|
+
if (trimmedSections.length > 0) {
|
|
101
|
+
console.log(
|
|
102
|
+
chalk.yellow(
|
|
103
|
+
` ⚠ Token budget: ${usedTokens}/${maxTokens} tokens. Trimmed: ${trimmedSections.join(", ")}`
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { assembledPrompt, totalTokens: usedTokens, trimmedSections };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Default Budgets ─────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/** Default context token budgets per provider. */
|
|
114
|
+
export const DEFAULT_TOKEN_BUDGETS: Record<string, number> = {
|
|
115
|
+
gemini: 900_000,
|
|
116
|
+
claude: 180_000,
|
|
117
|
+
openai: 120_000,
|
|
118
|
+
deepseek: 60_000,
|
|
119
|
+
default: 100_000,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export function getDefaultBudget(providerName: string): number {
|
|
123
|
+
return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
|
|
124
|
+
}
|
package/core/vcr.ts
CHANGED
|
@@ -141,13 +141,14 @@ export class VcrRecordingProvider implements AIProvider {
|
|
|
141
141
|
*/
|
|
142
142
|
export class VcrReplayProvider implements AIProvider {
|
|
143
143
|
private index = 0;
|
|
144
|
+
private _mismatches: Array<{ index: number; expected: string; actual: string }> = [];
|
|
144
145
|
|
|
145
146
|
constructor(private readonly recording: VcrRecording) {}
|
|
146
147
|
|
|
147
148
|
get providerName() { return "vcr-replay"; }
|
|
148
149
|
get modelName() { return this.recording.runId; }
|
|
149
150
|
|
|
150
|
-
async generate(
|
|
151
|
+
async generate(prompt: string, systemInstruction?: string): Promise<string> {
|
|
151
152
|
const entry = this.recording.entries[this.index++];
|
|
152
153
|
if (!entry) {
|
|
153
154
|
throw new Error(
|
|
@@ -155,11 +156,29 @@ export class VcrReplayProvider implements AIProvider {
|
|
|
155
156
|
`responses have been consumed. The pipeline made more AI calls than the recording has.`
|
|
156
157
|
);
|
|
157
158
|
}
|
|
159
|
+
|
|
160
|
+
// Validate prompt hash to detect pipeline drift
|
|
161
|
+
const actualHash = createHash("sha256")
|
|
162
|
+
.update(prompt + "\x00" + (systemInstruction ?? ""))
|
|
163
|
+
.digest("hex")
|
|
164
|
+
.slice(0, 8);
|
|
165
|
+
if (actualHash !== entry.callHash) {
|
|
166
|
+
this._mismatches.push({
|
|
167
|
+
index: entry.index,
|
|
168
|
+
expected: entry.callHash,
|
|
169
|
+
actual: actualHash,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
158
173
|
return entry.response;
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
get remaining() { return this.recording.entries.length - this.index; }
|
|
162
177
|
get consumed() { return this.index; }
|
|
178
|
+
|
|
179
|
+
/** Returns prompt hash mismatches detected during replay. */
|
|
180
|
+
get mismatches() { return this._mismatches; }
|
|
181
|
+
get hasMismatches() { return this._mismatches.length > 0; }
|
|
163
182
|
}
|
|
164
183
|
|
|
165
184
|
// ─── Loader helpers ───────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Project Constitution
|
|
2
|
+
|
|
3
|
+
## 1. 架构规则 (Architecture Rules)
|
|
4
|
+
- **分层架构**:项目采用三层架构 `routes → controllers → services → database`。
|
|
5
|
+
- **禁止跨层调用**:
|
|
6
|
+
- `routes` 层仅负责定义路由和中间件,不能包含业务逻辑。
|
|
7
|
+
- `controllers` 层负责接收请求、调用 `services` 和返回响应,不能直接操作数据库。
|
|
8
|
+
- `services` 层封装核心业务逻辑,是唯一允许直接调用数据访问层(如 Prisma)的层级。
|
|
9
|
+
- **模块组织**:功能模块按领域划分,每个模块包含独立的 `routes.ts`, `controller.ts`, `service.ts` 文件。
|
|
10
|
+
|
|
11
|
+
## 2. 命名规范 (Naming Conventions)
|
|
12
|
+
- **文件命名**:使用 `kebab-case`(如 `user-profile.controller.ts`)。
|
|
13
|
+
- **变量/函数**:使用 `camelCase`(如 `getUserById`)。
|
|
14
|
+
- **类/接口**:使用 `PascalCase`(如 `UserService`)。
|
|
15
|
+
- **路由路径**:使用 `kebab-case`,资源名词复数(如 `/api/v1/user-profiles`)。
|
|
16
|
+
|
|
17
|
+
## 3. API 规范 (API Patterns)
|
|
18
|
+
- **路由前缀**:
|
|
19
|
+
- 客户端 API:`/api/v1/...`
|
|
20
|
+
- 管理端 API:`/api/v1/admin/...`
|
|
21
|
+
- **统一响应结构**:
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"code": 200,
|
|
25
|
+
"message": "success",
|
|
26
|
+
"data": {}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
- **错误码规范**:
|
|
30
|
+
- `400`:客户端请求错误(参数错误、验证失败)
|
|
31
|
+
- `401`:未认证
|
|
32
|
+
- `403`:无权限
|
|
33
|
+
- `404`:资源不存在
|
|
34
|
+
- `500`:服务器内部错误
|
|
35
|
+
- **认证/鉴权**:使用 `auth.middleware.ts` 保护需要认证的路由,位于 `middleware/` 目录。
|
|
36
|
+
|
|
37
|
+
## 4. 数据层规范 (Data Layer Rules)
|
|
38
|
+
- **数据库访问**:仅允许在 `service` 层使用 Prisma Client 进行数据库操作。
|
|
39
|
+
- **模型命名**:Prisma model 使用 `PascalCase`(如 `UserProfile`),表名使用 `snake_case`(如 `user_profiles`)。
|
|
40
|
+
- **事务处理**:复杂业务逻辑使用 Prisma 的 `$transaction` API 保证数据一致性。
|
|
41
|
+
|
|
42
|
+
## 5. 错误处理规范 (Error Handling Patterns)
|
|
43
|
+
- **统一错误中间件**:使用 `error.middleware.ts` 捕获所有错误,位于 `middleware/` 目录。
|
|
44
|
+
- **错误抛出**:在 `service` 或 `controller` 中抛出 `AppError`(自定义错误类,包含 `code` 和 `message`)。
|
|
45
|
+
- **已知错误码**:
|
|
46
|
+
- `VALIDATION_ERROR` (400)
|
|
47
|
+
- `UNAUTHORIZED` (401)
|
|
48
|
+
- `FORBIDDEN` (403)
|
|
49
|
+
- `NOT_FOUND` (404)
|
|
50
|
+
- `INTERNAL_ERROR` (500)
|
|
51
|
+
|
|
52
|
+
## 6. 禁区 (Red Lines — Never Violate)
|
|
53
|
+
- [ ] **禁止**在 `controller` 或 `route` 层直接操作数据库(必须通过 `service`)。
|
|
54
|
+
- [ ] **禁止**创建重复的配置文件(如错误码、路由定义),必须追加到现有文件。
|
|
55
|
+
- [ ] **禁止**使用 `any` 类型,必须定义明确的接口或类型。
|
|
56
|
+
- [ ] **禁止**提交未通过 `vitest` 测试的代码。
|
|
57
|
+
|
|
58
|
+
## 7. 测试规范 (Testing Rules)
|
|
59
|
+
- **测试文件位置**:与源文件同目录,命名规则为 `*.test.ts` 或 `*.spec.ts`。
|
|
60
|
+
- **测试覆盖**:必须为所有 `service` 和 `controller` 方法编写单元测试,关键业务流程需包含集成测试。
|
|
61
|
+
- **测试框架**:使用 `vitest` 进行单元测试和集成测试,使用 `supertest` 进行 HTTP 接口测试。
|
|
62
|
+
|
|
63
|
+
## 8. 共享配置文件清单 (Shared Config Files — Append-Only)
|
|
64
|
+
|
|
65
|
+
(No shared config files detected — will be populated on first run)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "demo-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "ts-node src/index.ts",
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"test": "vitest run"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"express": "^4.18.2",
|
|
12
|
+
"cors": "^2.8.5"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.7.0",
|
|
16
|
+
"@types/express": "^4.17.21",
|
|
17
|
+
"@types/cors": "^2.8.17",
|
|
18
|
+
"ts-node": "^10.9.2",
|
|
19
|
+
"vitest": "^2.1.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// This is your Prisma schema file,
|
|
2
|
+
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
3
|
+
|
|
4
|
+
generator client {
|
|
5
|
+
provider = "prisma-client-js"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
datasource db {
|
|
9
|
+
provider = "postgresql"
|
|
10
|
+
url = env("DATABASE_URL")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
model Bookmark {
|
|
14
|
+
id String @id @default(uuid())
|
|
15
|
+
title String
|
|
16
|
+
url String
|
|
17
|
+
tags String[]
|
|
18
|
+
createdAt DateTime @default(now())
|
|
19
|
+
updatedAt DateTime @updatedAt
|
|
20
|
+
|
|
21
|
+
@@map("bookmarks")
|
|
22
|
+
}
|
package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"feature": {
|
|
4
|
+
"id": "bookmark-management-system",
|
|
5
|
+
"title": "Bookmark 管理系统",
|
|
6
|
+
"description": "实现个人书签管理系统,支持创建、查看、编辑、删除网页书签,通过标签分类和分页浏览。"
|
|
7
|
+
},
|
|
8
|
+
"models": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Bookmark",
|
|
11
|
+
"description": "书签数据模型,包含标题、URL和标签等信息",
|
|
12
|
+
"fields": [
|
|
13
|
+
{
|
|
14
|
+
"name": "id",
|
|
15
|
+
"type": "String",
|
|
16
|
+
"required": true,
|
|
17
|
+
"unique": true,
|
|
18
|
+
"description": ""
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "title",
|
|
22
|
+
"type": "String",
|
|
23
|
+
"required": true,
|
|
24
|
+
"unique": false,
|
|
25
|
+
"description": "书签标题"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "url",
|
|
29
|
+
"type": "String",
|
|
30
|
+
"required": true,
|
|
31
|
+
"unique": false,
|
|
32
|
+
"description": "书签URL,需为有效URL格式"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "tags",
|
|
36
|
+
"type": "String[]",
|
|
37
|
+
"required": false,
|
|
38
|
+
"unique": false,
|
|
39
|
+
"description": "标签数组"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "createdAt",
|
|
43
|
+
"type": "DateTime",
|
|
44
|
+
"required": true,
|
|
45
|
+
"unique": false,
|
|
46
|
+
"description": "创建时间"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "updatedAt",
|
|
50
|
+
"type": "DateTime",
|
|
51
|
+
"required": true,
|
|
52
|
+
"unique": false,
|
|
53
|
+
"description": "更新时间"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"relations": []
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"endpoints": [
|
|
60
|
+
{
|
|
61
|
+
"id": "EP-001",
|
|
62
|
+
"method": "GET",
|
|
63
|
+
"path": "/api/v1/bookmarks",
|
|
64
|
+
"description": "获取书签分页列表",
|
|
65
|
+
"auth": false,
|
|
66
|
+
"request": {
|
|
67
|
+
"body": {},
|
|
68
|
+
"query": {
|
|
69
|
+
"limit": "integer (positive, default 20)",
|
|
70
|
+
"offset": "integer (non-negative, default 0)"
|
|
71
|
+
},
|
|
72
|
+
"params": {}
|
|
73
|
+
},
|
|
74
|
+
"successStatus": 200,
|
|
75
|
+
"successDescription": "返回书签数组及总数",
|
|
76
|
+
"errors": [
|
|
77
|
+
{
|
|
78
|
+
"status": 400,
|
|
79
|
+
"code": "VALIDATION_ERROR",
|
|
80
|
+
"description": "查询参数验证失败,如 limit 或 offset 无效"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"status": 500,
|
|
84
|
+
"code": "INTERNAL_ERROR",
|
|
85
|
+
"description": "服务器内部错误"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "EP-002",
|
|
91
|
+
"method": "POST",
|
|
92
|
+
"path": "/api/v1/bookmarks",
|
|
93
|
+
"description": "创建新书签",
|
|
94
|
+
"auth": false,
|
|
95
|
+
"request": {
|
|
96
|
+
"body": {
|
|
97
|
+
"title": "string (required, non-empty)",
|
|
98
|
+
"url": "string (required, valid URL format)",
|
|
99
|
+
"tags": "string array (optional)"
|
|
100
|
+
},
|
|
101
|
+
"query": {},
|
|
102
|
+
"params": {}
|
|
103
|
+
},
|
|
104
|
+
"successStatus": 201,
|
|
105
|
+
"successDescription": "返回新创建的书签信息",
|
|
106
|
+
"errors": [
|
|
107
|
+
{
|
|
108
|
+
"status": 400,
|
|
109
|
+
"code": "VALIDATION_ERROR",
|
|
110
|
+
"description": "请求体验证失败,如 title 为空或 url 格式无效"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"status": 500,
|
|
114
|
+
"code": "INTERNAL_ERROR",
|
|
115
|
+
"description": "服务器内部错误"
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"id": "EP-003",
|
|
121
|
+
"method": "PUT",
|
|
122
|
+
"path": "/api/v1/bookmarks/:id",
|
|
123
|
+
"description": "全量更新指定书签",
|
|
124
|
+
"auth": false,
|
|
125
|
+
"request": {
|
|
126
|
+
"body": {
|
|
127
|
+
"title": "string (required)",
|
|
128
|
+
"url": "string (required, valid URL format)",
|
|
129
|
+
"tags": "string array (required)"
|
|
130
|
+
},
|
|
131
|
+
"query": {},
|
|
132
|
+
"params": {
|
|
133
|
+
"id": "string (UUID format)"
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"successStatus": 200,
|
|
137
|
+
"successDescription": "返回更新后的书签信息",
|
|
138
|
+
"errors": [
|
|
139
|
+
{
|
|
140
|
+
"status": 400,
|
|
141
|
+
"code": "VALIDATION_ERROR",
|
|
142
|
+
"description": "请求体验证失败"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"status": 404,
|
|
146
|
+
"code": "NOT_FOUND",
|
|
147
|
+
"description": "书签未找到"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"status": 500,
|
|
151
|
+
"code": "INTERNAL_ERROR",
|
|
152
|
+
"description": "服务器内部错误"
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"id": "EP-004",
|
|
158
|
+
"method": "DELETE",
|
|
159
|
+
"path": "/api/v1/bookmarks/:id",
|
|
160
|
+
"description": "删除指定书签",
|
|
161
|
+
"auth": false,
|
|
162
|
+
"request": {
|
|
163
|
+
"body": {},
|
|
164
|
+
"query": {},
|
|
165
|
+
"params": {
|
|
166
|
+
"id": "string (UUID format)"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"successStatus": 204,
|
|
170
|
+
"successDescription": "无响应体",
|
|
171
|
+
"errors": [
|
|
172
|
+
{
|
|
173
|
+
"status": 404,
|
|
174
|
+
"code": "NOT_FOUND",
|
|
175
|
+
"description": "书签未找到"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"status": 500,
|
|
179
|
+
"code": "INTERNAL_ERROR",
|
|
180
|
+
"description": "服务器内部错误"
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
"behaviors": []
|
|
186
|
+
}
|