Browse Source

feat: 更新路由系统并添加错误响应格式化功能

- 引入新的API和版本路由模块
- 实现错误响应格式化工具,支持不同格式的错误信息
- 优化路由中间件的组合方式,使用koa-compose简化中间件处理
alpha
npmrun 2 months ago
parent
commit
7d395f02bf
  1. 2
      .cursorindexingignore
  2. 65
      .specstory/.what-is-this.md
  3. 1441
      .specstory/history/2025-06-17_14-17-testing-the-chat-functionality.md
  4. 7
      src/controllers/userController.js
  5. 3
      src/controllers/v1/statusController.js
  6. 60
      src/main.js
  7. 9
      src/routes/apiRoutes.js
  8. 14
      src/routes/v1Routes.js
  9. 32
      src/utils/router.js

2
.cursorindexingignore

@ -0,0 +1,2 @@
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
.specstory/**

65
.specstory/.what-is-this.md

@ -0,0 +1,65 @@
# SpecStory Artifacts Directory
This directory is automatically created and maintained by the SpecStory extension to preserve your Cursor composer and chat history.
## What's Here?
- `.specstory/history`: Contains markdown files of your AI coding sessions
- Each file represents a separate chat or composer session
- Files are automatically updated as you work
- `.specstory/cursor_rules_backups`: Contains backups of the `.cursor/rules/derived-cursor-rules.mdc` file
- Backups are automatically created each time the `.cursor/rules/derived-cursor-rules.mdc` file is updated
- You can enable/disable the Cursor Rules feature in the SpecStory settings, it is disabled by default
## Valuable Uses
- Capture: Keep your context window up-to-date when starting new Chat/Composer sessions via @ references
- Search: For previous prompts and code snippets
- Learn: Meta-analyze your patterns and learn from your past experiences
- Derive: Keep Cursor on course with your past decisions by automatically deriving Cursor rules from your AI interactions
## Version Control
We recommend keeping this directory under version control to maintain a history of your AI interactions. However, if you prefer not to version these files, you can exclude them by adding this to your `.gitignore`:
```
.specstory
```
We recommend not keeping the `.specstory/cursor_rules_backups` directory under version control if you are already using git to version the `.cursor/rules` directory, and committing regularly. You can exclude it by adding this to your `.gitignore`:
```
.specstory/cursor_rules_backups
```
## Searching Your Codebase
When searching your codebase in Cursor, search results may include your previous AI coding interactions. To focus solely on your actual code files, you can exclude the AI interaction history from search results.
To exclude AI interaction history:
1. Open the "Find in Files" search in Cursor (Cmd/Ctrl + Shift + F)
2. Navigate to the "files to exclude" section
3. Add the following pattern:
```
.specstory/*
```
This will ensure your searches only return results from your working codebase files.
## Notes
- Auto-save only works when Cursor/sqlite flushes data to disk. This results in a small delay after the AI response is complete before SpecStory can save the history.
- Auto-save does not yet work on remote WSL workspaces.
## Settings
You can control auto-saving behavior in Cursor:
1. Open Cursor → Settings → VS Code Settings (Cmd/Ctrl + ,)
2. Search for "SpecStory"
3. Find "Auto Save" setting to enable/disable
Auto-save occurs when changes are detected in Cursor's sqlite database, or every 2 minutes as a safety net.

1441
.specstory/history/2025-06-17_14-17-testing-the-chat-functionality.md

File diff suppressed because it is too large

7
src/controllers/userController.js

@ -0,0 +1,7 @@
export async function hello(ctx) {
ctx.body = 'Hello World';
}
export async function getUser(ctx) {
ctx.body = `User ID: ${ctx.params.id}`;
}

3
src/controllers/v1/statusController.js

@ -0,0 +1,3 @@
export async function status(ctx) {
ctx.body = 'OK';
}

60
src/main.js

@ -3,7 +3,8 @@ import Koa from 'koa';
import os from 'os'; import os from 'os';
import log4js from "log4js" import log4js from "log4js"
import LoadPlugins from "./plugins/install.js" import LoadPlugins from "./plugins/install.js"
import Router from './utils/router.js'; import apiRoutes from './routes/apiRoutes.js';
import v1Routes from './routes/v1Routes.js';
const logger = log4js.getLogger() const logger = log4js.getLogger()
@ -11,44 +12,45 @@ const app = new Koa();
LoadPlugins(app) LoadPlugins(app)
// 错误响应格式化工具
function formatError(ctx, status, message) {
const accept = ctx.accepts('json', 'html', 'text');
if (accept === 'json') {
ctx.type = 'application/json';
ctx.body = { success: false, error: message };
} else if (accept === 'html') {
ctx.type = 'html';
ctx.body = `
<html>
<head><title>${status} Error</title></head>
<body>
<h1>${status} Error</h1>
<p>${message}</p>
</body>
</html>
`;
} else {
ctx.type = 'text';
ctx.body = `${status} - ${message}`;
}
ctx.status = status;
}
const router = new Router({ prefix: '/api' }); // 错误处理中间
// 基础路由
router.get('/hello', (ctx) => {
ctx.body = 'Hello World';
});
// 参数路由
router.get('/user/:id', (ctx) => {
ctx.body = `User ID: ${ctx.params.id}`;
});
// 路由组
router.group('/v1', (v1) => {
v1.use((ctx, next) => {
ctx.set('X-API-Version', 'v1');
return next();
});
v1.get('/status', (ctx) => ctx.body = 'OK');
});
app.use(router.middleware());
// 错误处理中间件
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
try { try {
await next(); await next();
if (ctx.status === 404) { if (ctx.status === 404) {
ctx.status = 404; formatError(ctx, 404, 'Resource not found');
ctx.body = { success: false, error: 'Resource not found' };
} }
} catch (err) { } catch (err) {
ctx.status = err.statusCode || 500; formatError(ctx, err.statusCode || 500, err.message || err || 'Internal server error');
ctx.body = { success: false, error: err.message || 'Internal server error' };
} }
}); });
app.use(apiRoutes.middleware());
app.use(v1Routes.middleware());
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => { const server = app.listen(PORT, () => {

9
src/routes/apiRoutes.js

@ -0,0 +1,9 @@
import Router from '../utils/router.js';
import { hello, getUser } from '../controllers/userController.js';
const router = new Router({ prefix: '/api' });
router.get('/hello', hello);
router.get('/user/:id', getUser);
export default router;

14
src/routes/v1Routes.js

@ -0,0 +1,14 @@
import Router from '../utils/router.js';
import { status } from '../controllers/v1/statusController.js';
const v1 = new Router({ prefix: '/api/v1' });
// 组内中间件
v1.use((ctx, next) => {
ctx.set('X-API-Version', 'v1');
return next();
});
v1.get('/status', status);
export default v1;

32
src/utils/router.js

@ -1,4 +1,5 @@
import { pathToRegexp } from 'path-to-regexp'; import { match } from 'path-to-regexp';
import compose from 'koa-compose';
class Router { class Router {
/** /**
@ -59,20 +60,19 @@ class Router {
*/ */
middleware() { middleware() {
return async (ctx, next) => { return async (ctx, next) => {
// 执行全局中间件
for (const middleware of this.middlewares) {
await middleware(ctx, next);
}
const { method, path } = ctx; const { method, path } = ctx;
const route = this._matchRoute(method.toLowerCase(), path); const route = this._matchRoute(method.toLowerCase(), path);
// 组合所有中间件和 handler
const middlewares = [...this.middlewares];
if (route) { if (route) {
ctx.params = route.params; ctx.params = route.params;
await route.handler(ctx, next); middlewares.push(route.handler);
} else {
await next();
} }
// 用 koa-compose 组合
const composed = compose(middlewares);
await composed(ctx, next);
}; };
} }
@ -83,8 +83,8 @@ class Router {
_registerRoute(method, path, handler) { _registerRoute(method, path, handler) {
const fullPath = this.prefix + path; const fullPath = this.prefix + path;
const keys = []; const keys = [];
const regexp = pathToRegexp(fullPath, keys).regexp; const matcher = match(fullPath, { decode: decodeURIComponent });
this.routes[method].push({ path: fullPath, regexp, keys, handler }); this.routes[method].push({ path: fullPath, matcher, keys, handler });
} }
/** /**
@ -94,13 +94,9 @@ class Router {
_matchRoute(method, currentPath) { _matchRoute(method, currentPath) {
const routes = this.routes[method] || []; const routes = this.routes[method] || [];
for (const route of routes) { for (const route of routes) {
const match = route.regexp.exec(currentPath); const matchResult = route.matcher(currentPath);
if (match) { if (matchResult) {
const params = {}; return { ...route, params: matchResult.params };
for (let i = 1; i < match.length; i++) {
params[route.keys[i - 1].name] = match[i] || '';
}
return { ...route, params };
} }
} }
return null; return null;

Loading…
Cancel
Save