Skip to content

Commit 0ba4561

Browse files
committed
[add] File Crawler API based on Web-file-cache template repository
[optimize] Markdown parser & Koa router [optimize] upgrade to MobX-GitHub 0.4 & other latest Upstream packages
1 parent 8090002 commit 0ba4561

File tree

15 files changed

+1583
-1416
lines changed

15 files changed

+1583
-1416
lines changed

.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ SENTRY_PROJECT =
99
NEXT_PUBLIC_LARK_API_HOST = https://open.feishu.cn/open-apis/
1010
NEXT_PUBLIC_LARK_APP_ID = cli_a2c7771153f8900c
1111
NEXT_PUBLIC_LARK_WIKI_URL = https://idea2app.feishu.cn/wiki/space/7318346900506181660
12+
13+
NEXT_PUBLIC_CACHE_HOST = https://cache.example.com
14+
CACHE_REPOSITORY = your-namespace/Web-file-cache

README.md

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,30 @@
1414
- Component suite: [Bootstrap v5][4]
1515
- PWA framework: [Workbox v6][5]
1616
- State management: [MobX v6][11]
17-
- API router: [Koa 2][12]
17+
- API router: [Koa 3][12]
1818
- CI / CD: GitHub [Actions][13] + [Vercel][14]
1919
- Monitor service: [Sentry][15]
2020

2121
## Major examples
2222

2323
1. [Markdown articles](pages/article/)
24-
2. [Editor components](pages/component.tsx)
25-
3. [Pagination table](pages/pagination.tsx)
26-
4. [Scroll list](pages/scroll-list.tsx)
27-
5. [Not Found page (NGO)](pages/_error.tsx)
24+
2. [Lark wiki](pages/wiki/)
25+
3. [Editor components](pages/component.tsx)
26+
4. [Pagination table](pages/pagination.tsx)
27+
5. [Scroll list](pages/scroll-list.tsx)
28+
6. [Not Found page (NGO)](pages/_error.tsx)
2829
- Global: https://notfound.org/
2930
- Chinese: https://www.dnpw.org/cn/pa-notfound.html
3031

3132
## User cases
3233

3334
1. https://github.com/kaiyuanshe/kaiyuanshe.github.io
3435
2. https://github.com/idea2app/idea2app.github.io
36+
3. https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io
3537

3638
## Best practice
3739

3840
1. Install GitHub apps in your organization or account:
39-
4041
1. [Probot settings][16]: set up Issue labels & Pull Request rules
4142
2. [PR badge][17]: set up Online [VS Code][18] editor entries in Pull Request description
4243

@@ -80,21 +81,24 @@ You can check out [the Next.js GitHub repository][28] - your feedback and contri
8081

8182
### Environment variables
8283

83-
| name | file | description |
84-
| :-----------------------: | :----------: | :-----------------------: |
85-
| `SENTRY_AUTH_TOKEN` | `.env.local` | [Official document][29] |
86-
| `SENTRY_ORG` | `.env` | [Official document][30] |
87-
| `SENTRY_PROJECT` | `.env` | [Official document][30] |
88-
| `NEXT_PUBLIC_SENTRY_DSN` | `.env` | [Official document][31] |
89-
| `NEXT_PUBLIC_LARK_APP_ID` | `.env.local` | [Official document][32] |
90-
| `LARK_APP_SECRET` | `.env.local` | [Official document][32] |
91-
| `NEXT_PUBLIC_CACHE_HOST` | `.env` | Static files CDN for Lark |
84+
| name | file | description |
85+
| :-------------------------: | :----------: | :---------------------------------------------: |
86+
| `JWT_SECRET` | `.env.local` | Random String for **JSON Web Token** encryption |
87+
| `SENTRY_AUTH_TOKEN` | `.env.local` | [Official document][29] |
88+
| `SENTRY_ORG` | `.env` | [Official document][30] |
89+
| `SENTRY_PROJECT` | `.env` | [Official document][30] |
90+
| `NEXT_PUBLIC_SENTRY_DSN` | `.env` | [Official document][31] |
91+
| `NEXT_PUBLIC_LARK_APP_ID` | `.env.local` | [Official document][32] |
92+
| `LARK_APP_SECRET` | `.env.local` | [Official document][32] |
93+
| `NEXT_PUBLIC_CACHE_HOST` | `.env` | Static files CDN for Lark |
94+
| `CACHE_REPOSITORY` | `.env` | [GitHub repository for Lark file cache][33] |
95+
| `NEXT_PUBLIC_LARK_WIKI_URL` | `.env` | Entry URL of a Lark wiki |
9296

9397
### Vercel
9498

9599
The easiest way to deploy your Next.js app is to use the [Vercel Platform][14] from the creators of Next.js.
96100

97-
Check out our [Next.js deployment documentation][33] for more details.
101+
Check out our [Next.js deployment documentation][34] for more details.
98102

99103
### Docker
100104

@@ -103,6 +107,14 @@ pnpm pack-image
103107
pnpm container
104108
```
105109

110+
### CDN
111+
112+
1. create & configure your own `CACHE_REPOSITORY` with https://github.com/idea2app/Web-file-cache
113+
2. set the `NEXT_PUBLIC_CACHE_HOST` environment variable to your CDN URL
114+
3. start your Lark-Next.js service, then copy the Crawler JWT from CLI console
115+
4. create an Automation Flow in your Lark BI Table to watch Attachment fields changing, then send an HTTP `POST` request to `https://your.next.js/api/file/crawler/task` with the Crawler JWT in the `Authorization` header
116+
5. your Lark file attachments will be cached in your CDN and loaded by your Lark-Next.js service automatically
117+
106118
[0]: https://www.larksuite.com/
107119
[1]: https://react.dev/
108120
[2]: https://www.typescriptlang.org/
@@ -136,4 +148,5 @@ pnpm container
136148
[30]: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-environment-variables
137149
[31]: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#create-initialization-config-files
138150
[32]: https://open.larksuite.com/document/server-docs/getting-started/api-access-token/app-access-token-development-guide#1f8b587c
139-
[33]: https://nextjs.org/docs/deployment
151+
[33]: https://github.com/idea2app/Web-file-cache?tab=readme-ov-file
152+
[34]: https://nextjs.org/docs/deployment

models/Base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ githubClient.use(({ request }, next) => {
2323
return next();
2424
});
2525

26+
export { githubClient };
27+
2628
export const repositoryStore = new RepositoryModel('idea2app');
2729

2830
type UploadedFile = Record<'originalname' | 'filename' | 'location', string>;

models/configuration.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ export const Name = process.env.NEXT_PUBLIC_SITE_NAME,
44
Summary = process.env.NEXT_PUBLIC_SITE_SUMMARY,
55
DefaultImage = process.env.NEXT_PUBLIC_LOGO!;
66

7-
export const { VERCEL_URL, GITHUB_TOKEN } = process.env;
7+
export const { VERCEL_URL, JWT_SECRET, GITHUB_TOKEN, CACHE_REPOSITORY } =
8+
process.env;
89

910
export const API_Host = isServer()
1011
? VERCEL_URL
1112
? `https://${VERCEL_URL}`
1213
: 'http://localhost:3000'
1314
: globalThis.location.origin;
1415

15-
export const CACHE_HOST = process.env.NEXT_PUBLIC_CACHE_HOST!;
16+
export const CACHE_HOST = process.env.NEXT_PUBLIC_CACHE_HOST!,
17+
CrawlerEmail = `[email protected]`;
1618

1719
export const LARK_API_HOST = `${API_Host}/api/Lark/`;
1820

next-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3+
/// <reference path="./.next/types/routes.d.ts" />
34

45
// NOTE: This file should not be edited
56
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

package.json

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,36 @@
1515
"@editorjs/list": "^2.0.8",
1616
"@editorjs/paragraph": "^2.11.7",
1717
"@editorjs/quote": "^2.7.6",
18-
"@koa/router": "^14.0.0",
19-
"@mdx-js/loader": "^3.1.0",
20-
"@mdx-js/react": "^3.1.0",
21-
"@next/mdx": "^15.4.5",
22-
"@sentry/nextjs": "^10.1.0",
23-
"copy-webpack-plugin": "^13.0.0",
24-
"core-js": "^3.45.0",
18+
"@mdx-js/loader": "^3.1.1",
19+
"@mdx-js/react": "^3.1.1",
20+
"@next/mdx": "^15.5.2",
21+
"@sentry/nextjs": "^10.9.0",
22+
"copy-webpack-plugin": "^13.0.1",
23+
"core-js": "^3.45.1",
2524
"editorjs-html": "^4.0.5",
2625
"file-type": "^21.0.0",
2726
"formidable": "^3.5.4",
2827
"idea-react": "^2.0.0-rc.13",
2928
"jsonwebtoken": "^9.0.2",
3029
"koa": "^3.0.1",
30+
"koa-jwt": "^4.0.4",
3131
"koajax": "^3.1.2",
32-
"less": "^4.4.0",
32+
"less": "^4.4.1",
3333
"less-loader": "^12.3.0",
3434
"lodash": "^4.17.21",
35-
"marked": "^16.1.2",
35+
"marked": "^16.2.1",
3636
"mime": "^4.0.7",
3737
"mobx": "^6.13.7",
38-
"mobx-github": "^0.3.11",
38+
"mobx-github": "^0.4.0",
3939
"mobx-i18n": "^0.7.1",
40-
"mobx-lark": "^2.4.0",
40+
"mobx-lark": "^2.4.1",
4141
"mobx-react": "^9.2.0",
4242
"mobx-react-helper": "^0.5.1",
4343
"mobx-restful": "^2.1.0",
44-
"mobx-restful-table": "^2.5.2",
45-
"next": "^15.4.5",
44+
"mobx-restful-table": "^2.5.3",
45+
"next": "^15.5.2",
4646
"next-pwa": "~5.6.0",
47-
"next-ssr-middleware": "^1.0.2",
47+
"next-ssr-middleware": "^1.0.3",
4848
"next-with-less": "^3.0.1",
4949
"prismjs": "^1.30.0",
5050
"react": "^19.1.1",
@@ -55,43 +55,44 @@
5555
"remark-frontmatter": "^5.0.0",
5656
"remark-gfm": "^4.0.1",
5757
"remark-mdx-frontmatter": "^5.2.0",
58-
"undici": "^7.13.0",
59-
"web-utility": "^4.5.1",
60-
"webpack": "^5.101.0",
61-
"yaml": "^2.8.0"
58+
"undici": "^7.15.0",
59+
"web-utility": "^4.5.3",
60+
"webpack": "^5.101.3",
61+
"yaml": "^2.8.1"
6262
},
6363
"devDependencies": {
6464
"@babel/plugin-proposal-decorators": "^7.28.0",
6565
"@babel/plugin-transform-typescript": "^7.28.0",
6666
"@babel/preset-react": "^7.27.1",
67-
"@cspell/eslint-plugin": "^9.2.0",
68-
"@eslint/compat": "^1.3.1",
67+
"@cspell/eslint-plugin": "^9.2.1",
68+
"@eslint/compat": "^1.3.2",
6969
"@eslint/eslintrc": "^3.3.1",
70-
"@eslint/js": "^9.32.0",
71-
"@next/eslint-plugin-next": "^15.4.5",
70+
"@eslint/js": "^9.34.0",
71+
"@next/eslint-plugin-next": "^15.5.2",
7272
"@softonus/prettier-plugin-duplicate-remover": "^1.1.2",
73-
"@stylistic/eslint-plugin": "^5.2.2",
73+
"@stylistic/eslint-plugin": "^5.3.1",
7474
"@types/eslint-config-prettier": "^6.11.3",
7575
"@types/formidable": "^3.4.5",
76+
"@types/jsonwebtoken": "^9.0.10",
7677
"@types/koa": "^3.0.0",
7778
"@types/koa__router": "^12.0.4",
7879
"@types/lodash": "^4.17.20",
7980
"@types/next-pwa": "^5.6.9",
80-
"@types/node": "^22.17.0",
81-
"@types/react": "^19.1.9",
82-
"eslint": "^9.32.0",
83-
"eslint-config-next": "^15.4.5",
81+
"@types/node": "^22.18.0",
82+
"@types/react": "^19.1.12",
83+
"eslint": "^9.34.0",
84+
"eslint-config-next": "^15.5.2",
8485
"eslint-config-prettier": "^10.1.8",
8586
"eslint-plugin-react": "^7.37.5",
8687
"eslint-plugin-simple-import-sort": "^12.1.1",
8788
"globals": "^16.3.0",
8889
"husky": "^9.1.7",
8990
"jiti": "^2.5.1",
90-
"lint-staged": "^16.1.4",
91+
"lint-staged": "^16.1.6",
9192
"prettier": "^3.6.2",
9293
"prettier-plugin-css-order": "^2.1.2",
9394
"typescript": "~5.9.2",
94-
"typescript-eslint": "^8.39.0"
95+
"typescript-eslint": "^8.42.0"
9596
},
9697
"resolutions": {
9798
"next": "$next"
Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { Context } from 'koa';
12
import { LarkPageData, TableRecord, TableRecordData } from 'mobx-lark';
23
import { DataObject } from 'mobx-restful';
3-
import { createKoaRouter } from 'next-ssr-middleware';
4+
import { createKoaRouter, withKoaRouter } from 'next-ssr-middleware';
45

5-
import { withSafeKoaRouter } from '../../../core';
6+
import { safeAPI } from '../../../core';
67
import { proxyLark, proxyLarkAll } from '../../core';
78

89
export const config = { api: { bodyParser: false } };
@@ -14,30 +15,38 @@ function filterData(fields: DataObject) {
1415
if (!/^\w+$/.test(key)) delete fields[key];
1516
}
1617

17-
router.get('/apps/:app/tables/:table/records/:record', async context => {
18-
const { status, body } =
19-
await proxyLark<TableRecordData<DataObject>>(context);
18+
router.get(
19+
'/apps/:app/tables/:table/records/:record',
20+
safeAPI,
21+
async (context: Context) => {
22+
const { status, body } =
23+
await proxyLark<TableRecordData<DataObject>>(context);
2024

21-
const { fields } = body!.data!.record;
25+
const { fields } = body!.data!.record;
2226

23-
filterData(fields);
27+
filterData(fields);
2428

25-
context.status = status;
26-
context.body = body;
27-
});
29+
context.status = status;
30+
context.body = body;
31+
},
32+
);
2833

29-
router.get('/apps/:app/tables/:table/records', async context => {
30-
const { status, body } =
31-
await proxyLark<LarkPageData<TableRecord<DataObject>>>(context);
34+
router.get(
35+
'/apps/:app/tables/:table/records',
36+
safeAPI,
37+
async (context: Context) => {
38+
const { status, body } =
39+
await proxyLark<LarkPageData<TableRecord<DataObject>>>(context);
3240

33-
const list = body!.data!.items || [];
41+
const list = body!.data!.items || [];
3442

35-
for (const { fields } of list) filterData(fields);
43+
for (const { fields } of list) filterData(fields);
3644

37-
context.status = status;
38-
context.body = body;
39-
});
45+
context.status = status;
46+
context.body = body;
47+
},
48+
);
4049

41-
router.all('/(.*)', proxyLarkAll);
50+
router.all('/(.*)', safeAPI, proxyLarkAll);
4251

43-
export default withSafeKoaRouter(router);
52+
export default withKoaRouter(router);
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { createKoaRouter } from 'next-ssr-middleware';
1+
import { createKoaRouter, withKoaRouter } from 'next-ssr-middleware';
22

3-
import { withSafeKoaRouter } from '../../../core';
3+
import { safeAPI } from '../../../core';
44
import { lark } from '../../core';
55

66
const router = createKoaRouter(import.meta.url);
77

8-
router.get('/:type/:id', async context => {
8+
router.get('/:type/:id', safeAPI, async context => {
99
const { type, id } = context.params;
1010

1111
const markdown = await lark.downloadMarkdown(`${type}/${id}`);
@@ -14,4 +14,4 @@ router.get('/:type/:id', async context => {
1414
context.body = markdown;
1515
});
1616

17-
export default withSafeKoaRouter(router);
17+
export default withKoaRouter(router);

pages/api/Lark/file/[id]/[name].ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { fileTypeFromStream } from 'file-type';
2+
import { Middleware } from 'koa';
23
import MIME from 'mime';
3-
import { createKoaRouter } from 'next-ssr-middleware';
4+
import { createKoaRouter, withKoaRouter } from 'next-ssr-middleware';
45
import { Readable } from 'stream';
56

67
import { CACHE_HOST } from '../../../../../models/configuration';
7-
import { withSafeKoaRouter } from '../../../core';
8+
import { safeAPI } from '../../../core';
89
import { lark } from '../../core';
910

1011
const router = createKoaRouter(import.meta.url);
1112

12-
router.all('/:id/:name', async context => {
13+
const downloader: Middleware = async context => {
1314
const { method, url, params, query } = context;
1415
const { id, name } = params;
1516

@@ -47,6 +48,10 @@ router.all('/:id/:name', async context => {
4748
if (method === 'GET')
4849
// @ts-expect-error Web type compatibility
4950
context.body = Readable.fromWeb(stream2);
50-
});
51+
};
5152

52-
export default withSafeKoaRouter(router);
53+
router
54+
.head('/:id/:name', safeAPI, downloader)
55+
.get('/:id/:name', safeAPI, downloader);
56+
57+
export default withKoaRouter(router);

pages/api/Lark/file/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import formidable from 'formidable';
33
import { readFile } from 'fs/promises';
44
import MIME from 'mime';
55
import { UploadTargetType } from 'mobx-lark';
6-
import { createKoaRouter } from 'next-ssr-middleware';
6+
import { createKoaRouter, withKoaRouter } from 'next-ssr-middleware';
77
import { parse } from 'path';
88

99
import { LARK_API_HOST } from '../../../../models/configuration';
10-
import { withSafeKoaRouter } from '../../core';
10+
import { safeAPI } from '../../core';
1111
import { lark } from '../core';
1212

1313
export const config = { api: { bodyParser: false } };
1414

1515
const router = createKoaRouter(import.meta.url);
1616

17-
router.post('/', async context => {
17+
router.post('/', safeAPI, async context => {
1818
const form = formidable();
1919

2020
const [{ parent_type, parent_node }, { file }] = await form.parse(
@@ -45,4 +45,4 @@ router.post('/', async context => {
4545
return (context.body = { link });
4646
});
4747

48-
export default withSafeKoaRouter(router);
48+
export default withKoaRouter(router);

0 commit comments

Comments
 (0)