@adep/cli 0.2.0 → 0.2.2
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/dist/index.js +128 -51
- package/dist/vite.js +72 -13
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -596,12 +596,12 @@ var init_generated = __esm({
|
|
|
596
596
|
"viteConfig",
|
|
597
597
|
"gitignore"
|
|
598
598
|
],
|
|
599
|
-
"source": "apps/countdown\
|
|
599
|
+
"source": "\u672C\u76EE\u5F55\u5373\u771F\u76F8\u6E90\uFF08\u539F apps/countdown \u5DF2\u79FB\u51FA\u4ED3\u5E93\uFF0C\u72EC\u7ACB\u7EF4\u62A4\u4E8E git.a.jajabjbj.top:6602/a/countdown-adep.git\uFF09"
|
|
600
600
|
},
|
|
601
601
|
"files": [
|
|
602
602
|
{
|
|
603
603
|
"path": "README.md",
|
|
604
|
-
"content": "# {{name}}\n\n\u5012\u6570\u65E5 / \u7EAA\u5FF5\u65E5\uFF08countdown \u6A21\u677F\uFF09\u2014\u2014\u7531\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\u300C\u5012\u6570\u65E5\u300D\u79FB\u690D\u7684 adep \u5168\u6808\u5E94\u7528\uFF1A\n`functions/` \u4E91\u51FD\u6570\uFF08auth / events + `_shared/` \u5171\u4EAB\u6A21\u5757\uFF09+ `web/src/` \u524D\u7AEF\u6E90\u7801\uFF08Vue 3 + Vite\uFF0C\nHome / Edit / Profile \u4E09\u9875 + OIDC \u56DE\u8C03\u9875\u3001\u81EA\u5B9A\u4E49 TabBar + \u5DE6\u6ED1\u5220\u9664 / \u957F\u6309\u7F6E\u9876 / \u4E0B\u62C9\u5237\u65B0\uFF0C\n\u754C\u9762\u4E0E\u5C0F\u7A0B\u5E8F\u5BF9\u9F50\uFF09\u3002vite \u5DE5\u7A0B\u6839\u5728**\u9879\u76EE\u6839**\uFF08`index.html` + `vite.config.ts` + \u6839 `package.json`\uFF09\u3002\n\n> \u672C\u6A21\u677F\
|
|
604
|
+
"content": "# {{name}}\n\n\u5012\u6570\u65E5 / \u7EAA\u5FF5\u65E5\uFF08countdown \u6A21\u677F\uFF09\u2014\u2014\u7531\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\u300C\u5012\u6570\u65E5\u300D\u79FB\u690D\u7684 adep \u5168\u6808\u5E94\u7528\uFF1A\n`functions/` \u4E91\u51FD\u6570\uFF08auth / events + `_shared/` \u5171\u4EAB\u6A21\u5757\uFF09+ `web/src/` \u524D\u7AEF\u6E90\u7801\uFF08Vue 3 + Vite\uFF0C\nHome / Edit / Profile \u4E09\u9875 + OIDC \u56DE\u8C03\u9875\u3001\u81EA\u5B9A\u4E49 TabBar + \u5DE6\u6ED1\u5220\u9664 / \u957F\u6309\u7F6E\u9876 / \u4E0B\u62C9\u5237\u65B0\uFF0C\n\u754C\u9762\u4E0E\u5C0F\u7A0B\u5E8F\u5BF9\u9F50\uFF09\u3002vite \u5DE5\u7A0B\u6839\u5728**\u9879\u76EE\u6839**\uFF08`index.html` + `vite.config.ts` + \u6839 `package.json`\uFF09\u3002\n\n> \u672C\u6A21\u677F\u76EE\u5F55\u662F\u81EA\u5305\u542B\u771F\u76F8\u6E90\uFF08`scripts/build-templates.mjs` \u7531\u6B64\u751F\u6210 `generated.ts` \u5FEB\u7167\uFF0C\n> \u6539\u6A21\u677F\u540E\u91CD\u8DD1\u811A\u672C\uFF09\u3002**\u5B8C\u6574\u7248**\u5012\u6570\u65E5\uFF089 \u51FD\u6570\u57DF + \u4F1A\u5458/\u88C2\u53D8/\u519C\u5386/\u63D0\u9192 + 102 \u9879\u6D4B\u8BD5\uFF09\u89C1\u72EC\u7ACB\u4ED3\u5E93 [a/countdown-adep](https://git.a.jajabjbj.top:6602/a/countdown-adep)\uFF0C\u53EF\u88AB\u5E73\u53F0\u76F4\u63A5 from-git \u5BFC\u5165\u3002\n\n## \u5FEB\u901F\u5F00\u59CB\n\n```bash\nnpm install # \u5355\u4E00\u5DE5\u7A0B\u6839\uFF1A@adep/cli + \u524D\u7AEF\u4F9D\u8D56\uFF08vue / vue-router / @adep/auth-client\uFF09+ \u6784\u5EFA\u94FE\nnpm run dev # \u5168\u6808\u5F00\u53D1\uFF1Avite \u8FDB\u7A0B\u5185\u542F\u52A8 adep dev + /api \u4EE3\u7406\uFF0C\u524D\u7AEF HMR + \u51FD\u6570\u70ED\u91CD\u8F7D\nnpm test # \u51FD\u6570/\u5DE5\u5177\u5171\u4EAB\u903B\u8F91\u5355\u5143\u6D4B\u8BD5\uFF08vitest\uFF09\nnpm run build # \u6784\u5EFA\u524D\u7AEF \u2192 site/\nnpm run start # \u72EC\u7ACB\u8FD0\u884C\uFF08\u672C\u5730\u6A21\u62DF\u8FD0\u884C\u65F6 + \u9759\u6001\u6258\u7BA1 + --schema \u81EA\u52A8\u5EFA\u8868\uFF09\uFF0C\u6D4F\u89C8\u5668\u5F00 http://127.0.0.1:8787\n```\n\n## \u90E8\u7F72\u5230 adep \u5E73\u53F0\n\n```bash\nadep login # \u767B\u5F55\uFF08\u5199 ~/.adep/credentials\uFF09\nnpm run deploy # \u53D1\u5E03\u4E91\u51FD\u6570\uFF08\u542B _shared/ \u5171\u4EAB\u6E90\u7801\uFF09\nnpm run build && npm run site:deploy # \u6784\u5EFA\u524D\u7AEF \u2192 \u4E0A\u4F20 site/ \u5230\u5E73\u53F0\u9759\u6001\u6258\u7BA1\uFF08\u5F00 SPA \u56DE\u9000\uFF09\n```\n\n\u53D1\u5E03\u540E\u8BBF\u95EE `https://{project-slug}.adep.localhost:3001`\uFF08\u672C\u5730 dev \u5E73\u53F0\uFF09\u6216\u7EBF\u4E0A\u7AD9\u70B9\u5730\u5740\u3002\n\n> \u524D\u7AEF\u4E3A\u4EC0\u4E48\u4E0D\u7528 `adep publish`\uFF08\u5168\u6808\uFF09\uFF1F\u90A3\u6761\u8DEF\u8D70\u5E73\u53F0\u300C\u524D\u7AEF\u8349\u7A3F\u6784\u5EFA\u300D\u7BA1\u7EBF\uFF0C\u800C\u5B83\u7684\u5165\u53E3\u95E8\u69DB\u662F\n> \u8349\u7A3F\u91CC\u5B58\u5728 `web/index.html`\uFF1B\u672C\u6A21\u677F\u7684\u89C4\u8303\u5F62\u6001\u662F\u300C\u6839 vite \u5DE5\u7A0B + `web/` \u53EA\u653E\u6E90\u7801\u300D\uFF0C\u8349\u7A3F\u91CC\n> \u6CA1\u6709\u8BE5\u6587\u4EF6\u3002\u524D\u7AEF\u4E0A\u7EBF\u7EDF\u4E00\u8D70 `vite build` \u2192 `site/` \u2192 `adep hosting deploy site --spa`\n> \uFF08\u4E0E fullstack \u6A21\u677F\u540C\u4E00\u53E3\u5F84\uFF09\u3002\n\n## \u72EC\u7ACB\u90E8\u7F72\uFF08\u4E0D\u4F9D\u8D56 adep \u5E73\u53F0\uFF09\n\n`npm run build && npm run start` \u5373\u53EF\u672C\u5730\u72EC\u7ACB\u8DD1\uFF08adep serve \u4E0E\u5E73\u53F0\u540C\u6E90\u6267\u884C\u5668\uFF09\u3002\n\u5BB9\u5668\u5316\u90E8\u7F72\u53C2\u8003\u72EC\u7ACB\u4ED3\u5E93 a/countdown-adep \u7684 `Dockerfile` + `compose.yaml`\uFF08\u5355\u5BB9\u5668 =\nserve \u627F\u8F7D /api/* + \u9759\u6001\u6258\u7BA1 site/ + .adep/sim/ \u6570\u636E\u5377\uFF09\u3002\n\n## \u76EE\u5F55\u7ED3\u6784\n\n- `adep.config.ts`\uFF1A\u9879\u76EE\u914D\u7F6E\uFF08name / template / functionsDir / functions_prefix \u9ED8\u8BA4 /api\uFF09\n- `index.html` / `vite.config.ts`\uFF1Avite \u5165\u53E3\u4E0E\u914D\u7F6E\uFF08\u5DE5\u7A0B\u6839 = \u9879\u76EE\u6839\uFF0C`adep()` \u63D2\u4EF6\u4EE3\u7406 /api/*\uFF0Cbuild \u8F93\u51FA `site/`\uFF09\n- `functions/`\uFF1A\u4E91\u51FD\u6570\u76EE\u5F55\u2014\u2014`auth.ts`\uFF08\u767B\u5F55/\u4F1A\u8BDD\uFF1AOIDC \u8EAB\u4EFD\u4F18\u5148\u3001device \u8EAB\u4EFD\u517C\u5BB9\uFF09\u3001`events.ts`\n \uFF08\u4E8B\u4EF6 CRUD + \u7F6E\u9876\u6392\u5E8F\uFF09\u3001`_shared/`\uFF08auth / store / date / response / route \u5171\u4EAB\u6A21\u5757\n + \u5BF9\u5E94 vitest \u5355\u6D4B\uFF0C\u5168\u9879\u76EE\u5355\u4EFD\uFF09\n- `database/schema.sql`\uFF1A\u6570\u636E\u5E93\u7ED3\u6784\uFF08users / events \u8868\uFF1B`adep db migrate database/schema.sql` \u5E94\u7528\uFF09\n- `web/src/`\uFF1A\u524D\u7AEF\u6E90\u7801\u2014\u2014`web/src/views/`\uFF08Home / Edit / Profile + `OidcCallbackView.vue`\uFF09\u3001\n `web/src/ui/`\uFF08TabBar / UiHost \u590D\u523B wx.showToast \u7B49\uFF09\u3001`web/src/lib/`\n \uFF08request / auth / config / date / events / oidc\uFF09\n- `web/src/lib/request.ts`\uFF1Afetch \u5C01\u88C5\uFF08\u4E1A\u52A1\u4FE1\u5C01 + 40001 \u81EA\u52A8\u5237\u65B0\u91CD\u8BD5\uFF09\n- `site/`\uFF1A`npm run build` \u7684\u524D\u7AEF\u6784\u5EFA\u4EA7\u7269\uFF08\u7AD9\u70B9\u6258\u7BA1\u76EE\u5F55\uFF0C\u5DF2 gitignore\uFF09\n- `tsconfig.json` / `adep.d.ts`\uFF1ATypeScript \u914D\u7F6E\u4E0E\u4E91\u51FD\u6570\u4E0A\u4E0B\u6587\u7C7B\u578B\u58F0\u660E\n\n## \u7528\u6237\u4F53\u7CFB\u8BF4\u660E\uFF08\u767B\u5F55\u94FE\u8DEF\uFF09\n\n- **OIDC \u767B\u5F55\uFF08\u767B\u5F55\u5373\u670D\u52A1\uFF0C\u4E3B\u94FE\u8DEF\uFF09**\uFF1A`web/src/lib/oidc.ts` \u63A5\u5165\u5E73\u53F0\u4E09\u65B9\u767B\u5F55\uFF08\u6388\u6743\u7801 +\n PKCE S256\uFF0C\u4F1A\u8BDD\u81EA\u7BA1 localStorage\uFF0Caccess token \u8FC7\u671F\u81EA\u52A8\u5237\u65B0\uFF09\uFF0C`OidcCallbackView.vue`\n \u5904\u7406\u6388\u6743\u56DE\u8C03\uFF1B`functions/_shared/auth.ts` \u628A\u7F51\u5173\u89E3\u6790\u7684 `ctx.user`\uFF08\u5E73\u53F0\u7528\u6237\uFF09\u4EE5\n `oidc:<\u5E73\u53F0\u7528\u6237 id>` \u5173\u8054\u672C\u5730 users \u8868\uFF08\u96F6\u8FC1\u79FB\uFF0C\u7F3A\u884C\u5373\u5EFA\uFF09\u3002\u672A\u767B\u5F55\u65F6\u6570\u636E\u5B58 localStorage\n \uFF08`lib/events.ts`\uFF09\uFF0C\u767B\u5F55\u540E\u540C\u6B65\u4E91\u7AEF\u3002\n- **\u8BBE\u5907\u8EAB\u4EFD\uFF08\u517C\u5BB9\u65E7\u5BA2\u6237\u7AEF / API \u8C03\u7528\uFF0C\u515C\u5E95\uFF09**\uFF1A`POST /auth/login` \u6362\u53D6 device_id \u4F5C\n Bearer \u51ED\u636E\uFF08\u7B49\u4EF7 API Key\uFF09\uFF1Bweb \u524D\u7AEF\u5DF2\u8FC1\u79FB OIDC\uFF0C\u4E0D\u518D\u8D70\u8BBE\u5907\u767B\u5F55\u3002\u672A\u767B\u5F55/\u5931\u6548\u8FD4\u56DE\n \u4E1A\u52A1\u7801 40001\uFF0C`request.ts` \u636E\u6B64\u6E05\u4F1A\u8BDD\u3001\u6709 OIDC \u4F1A\u8BDD\u5219\u5F3A\u5236\u5237\u65B0\u91CD\u8BD5\u4E00\u6B21\u3002\n\n## \u66F4\u591A\n\n- \u79FB\u690D\u7ECF\u9A8C\u4E0E\u9010\u9879\u5BF9\u9F50\u8BF4\u660E\uFF1Aadep \u6587\u6863\u7AD9\u300C\u5B9E\u6218\uFF1A\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\u79FB\u690D\u5012\u6570\u65E5\u300D\uFF08tutorial-countdown.md\uFF09\n- \u72EC\u7ACB\u90E8\u7F72\u5BB9\u5668\u914D\u7F6E\u89C1\u72EC\u7ACB\u4ED3\u5E93 a/countdown-adep\uFF08Dockerfile / compose.yaml\uFF09\n"
|
|
605
605
|
},
|
|
606
606
|
{
|
|
607
607
|
"path": "database/schema.sql",
|
|
@@ -609,39 +609,31 @@ var init_generated = __esm({
|
|
|
609
609
|
},
|
|
610
610
|
{
|
|
611
611
|
"path": "functions/_shared/auth.test.ts",
|
|
612
|
-
"content": "/**\n * \u8BF7\u6C42\u9274\u6743\u5355\u5143\u6D4B\u8BD5\uFF1AOIDC \u8EAB\u4EFD\uFF08ctx.user \u2192 `oidc:<\u5E73\u53F0\u7528\u6237 id>` \u6620\u5C04\uFF09\u4E0E\n * device_id \u53CC\u6765\u6E90\uFF08\u767B\u5F55\u5373\u670D\u52A1\uFF0C\u89C1 _shared/auth.ts\uFF09\u3002\n * \u8FD0\u884C\uFF1Anpm run test\n */\nimport { describe, it, expect } from 'vitest'\nimport type { CloudDb, FunctionContext } from '@adep/types'\nimport {\n parseBearer,\n
|
|
612
|
+
"content": "/**\n * \u8BF7\u6C42\u9274\u6743\u5355\u5143\u6D4B\u8BD5\uFF1AOIDC \u8EAB\u4EFD\uFF08ctx.user \u2192 `oidc:<\u5E73\u53F0\u7528\u6237 id>` \u6620\u5C04\uFF09\u4E0E\n * device_id \u53CC\u6765\u6E90\uFF08\u767B\u5F55\u5373\u670D\u52A1\uFF0C\u89C1 _shared/auth.ts\uFF09\uFF1BFN-029 \u8D77\u8986\u76D6 authGuard\n * \u4E2D\u95F4\u4EF6\u5F62\u6001\uFF08state \u80CC\u5305 + 401 \u77ED\u8DEF\u4FE1\u5C01\uFF09\u3002\n * \u8FD0\u884C\uFF1Anpm run test\n */\nimport { describe, it, expect } from 'vitest'\nimport type { CloudDb, FunctionContext } from '@adep/types'\nimport { isAdepHttpEnvelope } from '@adep/runtime/shared/http-envelope'\nimport {\n parseBearer,\n resolveUser,\n authGuard,\n OIDC_DEVICE_PREFIX,\n deviceIdOfOidcUser,\n} from './auth'\nimport { findUserByDeviceId } from './store'\nimport type { UserRow } from './store'\n\n/** \u5185\u5B58 CloudDb \u6869\uFF1A\u53EA\u5B9E\u73B0 store.ts / resolveUser \u7528\u5230\u7684\u64CD\u4F5C\uFF08users \u8868 where+first/insert\uFF09\u3002 */\nfunction makeMemoryDb(seed: UserRow[] = []): { db: CloudDb; users: UserRow[] } {\n const users: UserRow[] = [...seed]\n const db = {\n table(name: string) {\n let filters: Array<[string, unknown]> = []\n const chain = {\n select(): unknown {\n return chain\n },\n owned(): unknown {\n return chain\n },\n where(col: string, value: unknown): unknown {\n filters = [...filters, [col, value]]\n return chain\n },\n async first(): Promise<Record<string, unknown> | null> {\n const hit = users.find((u) =>\n filters.every(([col, v]) => (u as unknown as Record<string, unknown>)[col] === v)\n )\n return hit === undefined ? null : { ...hit }\n },\n async insert(row: Record<string, unknown>): Promise<void> {\n users.push(row as unknown as UserRow)\n },\n }\n void name\n return chain\n },\n } as unknown as CloudDb\n return { db, users }\n}\n\n/** \u7EC4\u88C5\u6700\u5C0F FunctionContext\uFF08cloud \u5E26 db\uFF0C\u5176\u4F59\u5B57\u6BB5\u6309\u7F3A\u7701\uFF09\u3002 */\nfunction ctxOf(db: CloudDb, overrides: Partial<FunctionContext> = {}): FunctionContext {\n return {\n method: 'GET',\n path: '/auth/me',\n query: {},\n headers: {},\n body: undefined,\n files: [],\n cloud: { db } as FunctionContext['cloud'],\n user: null,\n ...overrides,\n }\n}\n\ndescribe('deviceIdOfOidcUser', () => {\n it('\u4EE5 `oidc:` \u524D\u7F00 + \u5E73\u53F0\u7528\u6237 id \u7EC4\u6210\u672C\u5730 device_id', () => {\n expect(deviceIdOfOidcUser({ id: 'plat-123' })).toBe(`${OIDC_DEVICE_PREFIX}plat-123`)\n })\n})\n\ndescribe('resolveUser \xB7 OIDC \u8EAB\u4EFD\uFF08ctx.user\uFF09', () => {\n it('\u9996\u6B21\u8BBF\u95EE\u81EA\u52A8\u5EFA\u884C\uFF08\u6620\u5C04\u5230 oidc:<\u5E73\u53F0\u7528\u6237 id>\uFF0C\u6635\u79F0\u53D6 ctx.user.name\uFF09', async () => {\n const { db, users } = makeMemoryDb()\n const ctx = ctxOf(db, { user: { id: 'plat-1', name: '\u5C0F\u660E', email: 'm@x.com' } })\n\n const user = await resolveUser(db, ctx)\n expect(user?.device_id).toBe('oidc:plat-1')\n expect(user?.nickname).toBe('\u5C0F\u660E')\n expect(users).toHaveLength(1)\n\n // \u5DF2\u5EFA\u884C\uFF1A\u518D\u6B21\u8BBF\u95EE\u590D\u7528\u540C\u4E00\u884C\uFF0C\u4E0D\u91CD\u590D\u5EFA\n const again = await resolveUser(db, ctx)\n expect(again?.id).toBe(user?.id)\n expect(users).toHaveLength(1)\n })\n\n it('\u7F3A name \u65F6\u6635\u79F0\u56DE\u9000\u300C\u5E73\u53F0\u7528\u6237\u300D', async () => {\n const { db, users } = makeMemoryDb()\n const user = await resolveUser(db, ctxOf(db, { user: { id: 'plat-2' } }))\n expect(user?.nickname).toBe('\u5E73\u53F0\u7528\u6237')\n expect(users).toHaveLength(1)\n })\n\n it('\u5DF2\u5B58\u5728\u7684 OIDC \u6620\u5C04\u884C\u76F4\u63A5\u547D\u4E2D\uFF08\u4E0D\u67E5 device Bearer\uFF09', async () => {\n const existing: UserRow = {\n id: 'local-9',\n device_id: 'oidc:plat-9',\n nickname: '\u8001\u7528\u6237',\n avatar_url: '',\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n }\n const { db, users } = makeMemoryDb([existing])\n const user = await resolveUser(db, ctxOf(db, { user: { id: 'plat-9' } }))\n expect(user?.id).toBe('local-9')\n expect(users).toHaveLength(1)\n })\n})\n\ndescribe('resolveUser \xB7 \u8BBE\u5907\u8EAB\u4EFD\uFF08Bearer device_id\uFF09', () => {\n it('\u5408\u6CD5 device_id \u547D\u4E2D\u7528\u6237', async () => {\n const row: UserRow = {\n id: 'local-1',\n device_id: 'dev-abc-123',\n nickname: '\u672C\u5730\u7528\u6237',\n avatar_url: '',\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n }\n const { db } = makeMemoryDb([row])\n const ctx = ctxOf(db, {\n headers: { authorization: 'Bearer dev-abc-123' },\n user: null,\n })\n const user = await resolveUser(db, ctx)\n expect(user?.id).toBe('local-1')\n })\n\n it('\u65E0\u51ED\u636E \u2192 null\uFF08\u8C03\u7528\u65B9\u51B3\u5B9A\u5931\u8D25\u54CD\u5E94\uFF09', async () => {\n const { db } = makeMemoryDb()\n expect(await resolveUser(db, ctxOf(db))).toBeNull()\n })\n\n it('token \u67E5\u65E0\u7528\u6237 \u2192 null', async () => {\n const { db } = makeMemoryDb()\n expect(\n await resolveUser(db, ctxOf(db, { headers: { authorization: 'Bearer ghost' } }))\n ).toBeNull()\n })\n})\n\ndescribe('authGuard \xB7 \u4E2D\u95F4\u4EF6\u5F62\u6001\uFF08FN-029\uFF09', () => {\n it('\u767B\u5F55\u6210\u529F\uFF1A\u7528\u6237\u884C\u5199\u5165 req.state\uFF0C\u8FD4\u56DE undefined \u653E\u884C', async () => {\n const { db } = makeMemoryDb([\n {\n id: 'local-1',\n device_id: 'dev-abc-123',\n nickname: '\u672C\u5730\u7528\u6237',\n avatar_url: '',\n created_at: null,\n updated_at: null,\n },\n ])\n const guard = authGuard()\n const req = {\n method: 'GET',\n path: '/events',\n params: {},\n query: {},\n raw: ctxOf(db, { headers: { authorization: 'Bearer dev-abc-123' } }),\n state: {} as Record<string, unknown>,\n }\n const short = await guard(req)\n expect(short).toBeUndefined()\n expect((req.state.user as UserRow).id).toBe('local-1')\n })\n\n it('\u672A\u767B\u5F55\uFF1A\u77ED\u8DEF\u8FD4\u56DE 40001 \u4FE1\u5C01\uFF08401 + code 40001\uFF09', async () => {\n const { db } = makeMemoryDb()\n const guard = authGuard()\n const short = await guard({\n method: 'GET',\n path: '/events',\n params: {},\n query: {},\n raw: ctxOf(db),\n state: {},\n })\n expect(isAdepHttpEnvelope(short)).toBe(true)\n if (isAdepHttpEnvelope(short)) {\n expect(short.__adepHttp.status).toBe(401)\n expect(short.__adepHttp.body).toMatchObject({ code: 40001, data: null })\n }\n })\n})\n\ndescribe('parseBearer', () => {\n it('\u5927\u5C0F\u5199 / \u591A\u4F59\u7A7A\u683C\u5BB9\u9519', () => {\n const mk = () => makeMemoryDb().db\n expect(parseBearer(ctxOf(mk(), { headers: { authorization: 'Bearer abc' } }))).toBe('abc')\n expect(parseBearer(ctxOf(mk(), { headers: { Authorization: ' bearer xyz ' } }))).toBe('xyz')\n expect(parseBearer(ctxOf(mk()))).toBe('')\n })\n})\n\ndescribe('store \xB7 OIDC \u6620\u5C04\u884C\u53EF\u88AB\u67E5\u8BE2', () => {\n it('findUserByDeviceId \u80FD\u627E\u5230 oidc: \u524D\u7F00\u884C\uFF08\u51FD\u6570\u5C42\u4E0E\u6570\u636E\u5C42\u4E00\u81F4\uFF09', async () => {\n const { db, users } = makeMemoryDb()\n const ctx = ctxOf(db, { user: { id: 'plat-7', name: '\u6D4B' } })\n const user = await resolveUser(db, ctx)\n expect(user).not.toBeNull()\n const found = await findUserByDeviceId(db, user?.device_id ?? '')\n expect(found?.id).toBe(user?.id)\n expect(users.some((u) => u.device_id.startsWith('oidc:'))).toBe(true)\n })\n})\n"
|
|
613
613
|
},
|
|
614
614
|
{
|
|
615
615
|
"path": "functions/_shared/auth.ts",
|
|
616
|
-
"content": "/**\n * \u8BF7\u6C42\u9274\u6743\uFF08\u79FB\u690D\u81EA cf-backend src/middleware/auth.ts + src/lib/jwt.ts \u7684\u8BED\u4E49\uFF09\u3002\n *\n * \u53CC\u8EAB\u4EFD\u6765\u6E90\uFF08\u767B\u5F55\u5373\u670D\u52A1\uFF0C\u63A5\u7EBF\u89C1
|
|
616
|
+
"content": "/**\n * \u8BF7\u6C42\u9274\u6743\uFF08\u79FB\u690D\u81EA cf-backend src/middleware/auth.ts + src/lib/jwt.ts \u7684\u8BED\u4E49\uFF09\u3002\n *\n * \u53CC\u8EAB\u4EFD\u6765\u6E90\uFF08\u767B\u5F55\u5373\u670D\u52A1\uFF0C\u63A5\u7EBF\u89C1\u6A21\u677F README\u300C\u7528\u6237\u4F53\u7CFB\u8BF4\u660E\u300D\uFF09\uFF1A\n * 1. OIDC \u8EAB\u4EFD\uFF08\u4F18\u5148\uFF09\uFF1A\u7F51\u5173\u628A `Authorization: Bearer <oauth token>` \u89E3\u6790\u6210\n * `ctx.user`\uFF08\u5E73\u53F0\u7528\u6237\uFF0C\u89C1 server/wiring.ts resolveUser\uFF09\u3002\u672C\u5730 users \u8868\u7684\n * device_id \u590D\u7528\u4E3A `oidc:<\u5E73\u53F0\u7528\u6237 id>` \u5173\u8054\uFF08\u96F6\u8FC1\u79FB\uFF0C\u65E0\u9700\u6539\u8868\uFF09\uFF0C\u9996\u6B21\u8BBF\u95EE\u81EA\u52A8\u5EFA\u884C\u3002\n * 2. \u8BBE\u5907\u8EAB\u4EFD\uFF08\u517C\u5BB9\u65E7\u5BA2\u6237\u7AEF / API \u8C03\u7528\uFF09\uFF1A`POST /auth/login` \u6362\u53D6 device_id \u4F5C Bearer\n * \u51ED\u636E\uFF08web \u524D\u7AEF\u5DF2\u8FC1\u79FB OIDC \u767B\u5F55\uFF0C\u4E0D\u518D\u4F7F\u7528\uFF09\u3002device_id \u5373\u7528\u6237\u8868\u552F\u4E00\u952E\uFF0C\u7B49\u4EF7 API\n * Key\uFF1A\u67E5\u5F97\u5230 \u2192 \u8EAB\u4EFD\u6210\u7ACB\u3002\n *\n * FN-029 \u8D77\u4EE5**\u8DEF\u7531\u4E2D\u95F4\u4EF6**\u5F62\u6001\u63D0\u4F9B\uFF08`authGuard`\uFF09\uFF1A\u6CE8\u518C\u4E00\u6B21\uFF0C\u4E1A\u52A1 handler \u91CC\n * `req.state.user` \u76F4\u63A5\u62FF\u5230\u7528\u6237\u884C\u2014\u2014\u4E0D\u518D\u6BCF\u4E2A\u7AEF\u70B9\u624B\u5199 `isEnvelope` \u6536\u7A84\u6837\u677F\u3002\n * \u5931\u8D25\u77ED\u8DEF\u8FD4\u56DE\u4E0E\u5C0F\u7A0B\u5E8F\u540E\u7AEF\u4E00\u81F4\u7684\u4E1A\u52A1\u7801 40001\uFF08\u524D\u7AEF request.ts \u4F1A\u636E\u6B64\u9759\u9ED8\u91CD\u767B\uFF09\u3002\n */\nimport type { CloudDb, FunctionContext } from '@adep/types'\nimport { fail, CODE } from '@adep/runtime/response'\nimport type { RouteMiddleware } from '@adep/runtime/route'\nimport { findUserByDeviceId, createUser, type NewUserRow, type UserRow } from './store'\nimport { nowIso } from './date'\n\n/** OIDC \u7528\u6237\u5728\u672C\u5730 users \u8868 device_id \u7684\u5B58\u50A8\u524D\u7F00\uFF08`oidc:<\u5E73\u53F0\u7528\u6237 id>`\uFF09\u3002 */\nexport const OIDC_DEVICE_PREFIX = 'oidc:'\n\n/** \u5E73\u53F0\u7528\u6237\uFF08ctx.user\uFF09\u2192 \u672C\u5730 device_id\uFF08\u96F6\u8FC1\u79FB\u590D\u7528\u552F\u4E00\u952E\uFF09\u3002 */\nexport function deviceIdOfOidcUser(user: { id: string }): string {\n return `${OIDC_DEVICE_PREFIX}${user.id}`\n}\n\n/** \u89E3\u6790 Authorization: Bearer xxx\uFF08\u5E26\u524D\u5BFC\u5927\u5C0F\u5199/\u591A\u4F59\u7A7A\u683C\u5BB9\u9519\uFF09 */\nexport function parseBearer(ctx: FunctionContext): string {\n const header = ctx.headers?.authorization ?? ctx.headers?.Authorization ?? ''\n const match = /^Bearer\\s+(.+)$/i.exec(String(header).trim())\n return match?.[1]?.trim() ?? ''\n}\n\n/**\n * \u4ECE\u8BF7\u6C42\u89E3\u6790\u51FA\u7528\u6237\uFF1B\u7F3A\u51ED\u636E/\u7528\u6237\u4E0D\u5B58\u5728 \u2192 null\uFF08\u8C03\u7528\u65B9\u81EA\u884C\u51B3\u5B9A\u5931\u8D25\u54CD\u5E94\uFF09\u3002\n *\n * \u767B\u5F55\u5373\u670D\u52A1\uFF1AOIDC \u8EAB\u4EFD\uFF08ctx.user\uFF09\u4F18\u5148\u2014\u2014\u7F51\u5173\u5DF2\u628A OAuth Bearer \u89E3\u6790\u6210\u5E73\u53F0\u7528\u6237\uFF0C\n * \u672C\u5730\u884C\u4EE5 `oidc:<\u5E73\u53F0\u7528\u6237 id>` \u5173\u8054\uFF0C\u7F3A\u884C\u5373\u5EFA\uFF08\u5E42\u7B49\uFF1B\u6635\u79F0\u4EC5\u5EFA\u884C\u65F6\u5199\u5165\uFF0C\u4E0D\u56DE\u5199\uFF09\uFF1B\n * \u672A\u547D\u4E2D\u518D\u56DE\u9000 device_id \u51ED\u636E\uFF08web \u7AEF\u672A\u767B\u5F55\u7684\u672C\u5730\u8EAB\u4EFD\uFF09\u3002\n */\nexport async function resolveUser(db: CloudDb, ctx: FunctionContext): Promise<UserRow | null> {\n if (ctx.user !== null) {\n const deviceId = deviceIdOfOidcUser(ctx.user)\n let user = await findUserByDeviceId(db, deviceId)\n if (user === null) {\n const fresh: NewUserRow = {\n id: crypto.randomUUID(),\n device_id: deviceId,\n nickname: ctx.user.name ?? '\u5E73\u53F0\u7528\u6237',\n avatar_url: '',\n created_at: nowIso(),\n updated_at: nowIso(),\n }\n await createUser(db, fresh)\n user = fresh\n }\n return user\n }\n\n const token = parseBearer(ctx)\n if (!token) return null\n return findUserByDeviceId(db, token)\n}\n\n/**\n * \u9274\u6743\u4E2D\u95F4\u4EF6\uFF1A\u89E3\u6790\u5931\u8D25\u77ED\u8DEF 401\uFF0840001 \u4FE1\u5C01\uFF09\uFF1B\u6210\u529F\u628A\u7528\u6237\u884C\u653E\u8FDB `req.state.user`\u3002\n *\n * db \u76F4\u63A5\u4ECE `req.raw`\uFF08FunctionContext\uFF09\u53D6\u2014\u2014\u4E2D\u95F4\u4EF6\u4E0E handler \u770B\u5230\u7684\u662F\u540C\u4E00\u4E2A ctx\uFF0C\n * \u4E0D\u5FC5\u5404\u81EA\u4F20\u4E00\u904D\u3002\u7528\u6CD5\uFF1A`router.use(authGuard())`\uFF0C\u6CE8\u518C\u4E4B\u540E\u7684\u6240\u6709\u8DEF\u7531\uFF0Chandler \u91CC\n * `req.state.user as UserRow` \u5373\u5DF2\u767B\u5F55\u7528\u6237\uFF08\u4E1A\u52A1\u51FD\u6570\u91CC\u4E0D\u518D\u51FA\u73B0\u4FE1\u5C01\u6536\u7A84\u6837\u677F\uFF09\u3002\n */\nexport function authGuard(): RouteMiddleware<FunctionContext> {\n return async (req) => {\n const ctx = req.raw\n const db = ctx.cloud.db\n const user = await resolveUser(db, ctx)\n if (user === null) {\n return fail(\n CODE.NO_AUTH,\n parseBearer(ctx) === '' ? '\u672A\u767B\u5F55' : '\u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55'\n )\n }\n req.state.user = user\n return undefined\n }\n}\n"
|
|
617
617
|
},
|
|
618
618
|
{
|
|
619
619
|
"path": "functions/_shared/date.test.ts",
|
|
620
|
-
"content": "/**\n * \u670D\u52A1\u7AEF\u65E5\u671F/\u6392\u5E8F
|
|
620
|
+
"content": "/**\n * \u670D\u52A1\u7AEF\u65E5\u671F/\u6392\u5E8F\u5355\u5143\u6D4B\u8BD5\uFF08\u79FB\u690D\u903B\u8F91\u7684\u56DE\u5F52\u4FDD\u62A4\uFF09\u3002\n * \u54CD\u5E94\u4FE1\u5C01\u7684\u6D4B\u8BD5\u5728\u5B98\u65B9\u5305 `@adep/runtime` \u4FA7\uFF08FN-029 \u8D77 ok/fail \u6536\u7F16\u8FDB\u5E73\u53F0\uFF09\u3002\n * \u8FD0\u884C\uFF1Anpm run test\n */\nimport { describe, it, expect } from 'vitest'\nimport { isValidDate, calcDays, toDTO, sortEvents } from './date'\nimport type { EventRow } from './store'\n\ndescribe('isValidDate', () => {\n it('\u63A5\u53D7\u5408\u6CD5 YYYY-MM-DD', () => {\n expect(isValidDate('2026-09-20')).toBe(true)\n expect(isValidDate('2024-02-29')).toBe(true)\n })\n it('\u62D2\u7EDD\u975E\u6CD5\u65E5\u671F\u4E0E\u683C\u5F0F', () => {\n expect(isValidDate('2026-13-01')).toBe(false)\n expect(isValidDate('2026-02-30')).toBe(false)\n expect(isValidDate('2026/09/20')).toBe(false)\n expect(isValidDate('')).toBe(false)\n expect(isValidDate(null)).toBe(false)\n })\n})\n\ndescribe('calcDays\uFF08\u5317\u4EAC\u65F6\u95F4\u81EA\u7136\u65E5\uFF09', () => {\n it('\u5012\u8BA1\u65F6\uFF1A\u672A\u6765\u4E3A\u6B63\uFF0C\u5F53\u5929\u4E3A 0', () => {\n const today = new Date()\n const beijing = new Date(today.getTime() + 8 * 3600_000)\n const todayStr = `${beijing.getUTCFullYear()}-${String(beijing.getUTCMonth() + 1).padStart(2, '0')}-${String(\n beijing.getUTCDate()\n ).padStart(2, '0')}`\n expect(calcDays(todayStr)).toBe(0)\n })\n it('\u8FC7\u53BB\u65E5\u671F\u4E3A\u8D1F\uFF1Bcountup \u53D6\u7EDD\u5BF9\u503C', () => {\n expect(calcDays('2020-01-01')).toBeLessThan(0)\n expect(calcDays('2020-01-01', 'countup')).toBeGreaterThan(0)\n })\n it('\u975E\u6CD5\u65E5\u671F\u8FD4\u56DE 0', () => {\n expect(calcDays('bad-date')).toBe(0)\n })\n})\n\ndescribe('toDTO + sortEvents', () => {\n const base = {\n id: '1',\n user_id: 'u1',\n title: '\u4E8B\u4EF6',\n target_date: '2030-01-01',\n note: null,\n category: null,\n direction: 'countdown',\n is_pinned: 0,\n sort_order: 0,\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n } as EventRow\n\n it('toDTO \u8865\u9ED8\u8BA4\u503C\u5E76\u7B97 days', () => {\n const dto = toDTO(base)\n expect(dto.note).toBe('')\n expect(dto.category).toBe('other')\n expect(dto.is_pinned).toBe(false)\n expect(dto.days).toBeGreaterThan(0)\n })\n\n it('\u6392\u5E8F\uFF1A\u7F6E\u9876\u4F18\u5148\uFF0C\u5176\u4F59\u6309\u5269\u4F59\u5929\u6570\u5347\u5E8F\uFF08\u8FC7\u53BB\u6392\u6700\u540E\uFF09', () => {\n const far = { ...base, id: 'far', target_date: '2031-01-01' } as EventRow\n const near = { ...base, id: 'near', target_date: '2026-10-01' } as EventRow\n const past = { ...base, id: 'past', target_date: '2020-01-01' } as EventRow\n const pinnedFar = { ...far, id: 'pinnedFar', is_pinned: 1 } as EventRow\n const sorted = sortEvents([far, near, past, pinnedFar].map((r) => toDTO(r)))\n expect(sorted.map((e) => e.id)).toEqual(['pinnedFar', 'near', 'far', 'past'])\n })\n})\n"
|
|
621
621
|
},
|
|
622
622
|
{
|
|
623
623
|
"path": "functions/_shared/date.ts",
|
|
624
|
-
"content": "/**\n * \u670D\u52A1\u7AEF\u65E5\u671F\u5DE5\u5177\uFF08\u79FB\u690D\u81EA cf-backend src/lib/date.ts\uFF09\u3002\n * \u4E0E\u5C0F\u7A0B\u5E8F utils/date.js \u7B97\u6CD5\u5BF9\u9F50\uFF1A\u5929\u6570\u6309\u300C\u5317\u4EAC\u65F6\u95F4\uFF08UTC+8\uFF09\u81EA\u7136\u65E5\u300D\u8BA1\u7B97\u3002\n */\nconst WEEK = ['\u5468\u65E5', '\u5468\u4E00', '\u5468\u4E8C', '\u5468\u4E09', '\u5468\u56DB', '\u5468\u4E94', '\u5468\u516D']\n\n/** \u5317\u4EAC\u65F6\u95F4\u4ECA\u5929 YYYY-MM-DD\uFF08\u81EA\u7136\u65E5\uFF0C\u4E0D\u4EE5 UTC \u65E5\u671F\u4E3A\u51C6\uFF09 */\nexport function todayStr(): string {\n const now = new Date(Date.now() + 8 * 3600_000)\n const y = now.getUTCFullYear()\n const m = now.getUTCMonth() + 1\n const d = now.getUTCDate()\n return fmt(y, m, d)\n}\n\nfunction fmt(y: number, m: number, d: number): string {\n return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`\n}\n\nexport function isValidDate(s: unknown): s is string {\n if (typeof s !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return false\n const [y, m, d] = s.split('-').map(Number)\n if (m < 1 || m > 12 || d < 1 || d > 31) return false\n const dt = new Date(y, m - 1, d)\n return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d\n}\n\n/** \u6309\u5317\u4EAC\u65F6\u95F4\u81EA\u7136\u65E5\u5DEE\uFF08\u6B63\u6570\u672A\u6765 / \u8D1F\u6570\u8FC7\u53BB\uFF1Bcountup \u53D6\u7EDD\u5BF9\u503C\uFF09 */\nexport function calcDays(\n targetDate: string,\n direction: 'countdown' | 'countup' = 'countdown'\n): number {\n if (!isValidDate(targetDate)) return 0\n const target = new Date(`${targetDate}T00:00:00+08:00`).getTime()\n const now = new Date()\n const nowBeijing = new Date(now.getTime() + 8 * 3600_000)\n const todayStart = new Date(\n Date.UTC(nowBeijing.getUTCFullYear(), nowBeijing.getUTCMonth(), nowBeijing.getUTCDate()) -\n 8 * 3600_000\n ).getTime()\n const diff = Math.round((target - todayStart) / 86400000)\n return direction === 'countup' ? Math.abs(diff) : diff\n}\n\nexport function weekdayOf(targetDate: string): string {\n if (!isValidDate(targetDate)) return ''\n const [y, m, d] = targetDate.split('-').map(Number)\n return WEEK[new Date(y, m - 1, d).getDay()]\n}\n\n/** \u670D\u52A1\u5668\u65F6\u95F4 ISO\uFF08UTC\uFF0C\u5B58\u50A8\u7528\uFF09 */\nexport function nowIso(): string {\n return new Date().toISOString()\n}\n\n/** \u4E8B\u4EF6\u884C \u2192 DTO\uFF08\u9644 days / weekday\uFF1Bis_pinned \u5E03\u5C14\u5316\uFF09
|
|
625
|
-
},
|
|
626
|
-
{
|
|
627
|
-
"path": "functions/_shared/response.ts",
|
|
628
|
-
"content": "/**\n * \u7EDF\u4E00\u54CD\u5E94\u4FE1\u5C01\uFF08\u9010\u884C\u79FB\u690D\u81EA cf-backend src/lib/response.ts\uFF0C\u8F93\u51FA adep `__adepHttp` \u4FE1\u5C01\uFF09\u3002\n *\n * \u5C0F\u7A0B\u5E8F\u540E\u7AEF\u7528 Hono \u8FD4\u56DE `{code, data, message}` + HTTP \u72B6\u6001\uFF1Badep \u4E91\u51FD\u6570\u8FD4\u56DE\n * `__adepHttp` \u4FE1\u5C01\u5373\u53EF\u83B7\u5F97\u540C\u6837\u7684 status/body \u8BED\u4E49\uFF08ADR-0024 / CF-010\uFF0C\n * \u672C\u5730 dev \u4E0E\u5E73\u53F0\u7F51\u5173\u540C\u4E00\u5957\u8BC6\u522B\u903B\u8F91\uFF09\u3002\n */\nimport type { AdepHttpEnvelope } from '@adep/types'\n\nexport const CODE = {\n OK: 0,\n NO_AUTH: 40001,\n BAD_PARAM: 40002,\n NOT_FOUND: 40003,\n LIMIT_EXCEEDED: 40004,\n SERVER_ERROR: 50000,\n} as const\n\nexport type CodeValue = (typeof CODE)[keyof typeof CODE]\n\nconst HTTP_STATUS: Record<number, number> = {\n 0: 200,\n 40001: 401,\n 40002: 400,\n 40003: 404,\n 40004: 409,\n 50000: 500,\n}\n\nexport interface ApiBody<T> {\n code: number\n data: T | null\n message: string\n}\n\nconst JSON_HEADERS = { 'content-type': 'application/json; charset=utf-8' }\n\nfunction envelope<T>(body: ApiBody<T>, status: number): AdepHttpEnvelope {\n return {\n __adepHttp: {\n status,\n headers: JSON_HEADERS,\n body,\n },\n }\n}\n\nexport function ok<T>(data: T, message = 'ok'): AdepHttpEnvelope {\n return envelope({ code: CODE.OK, data, message }, 200)\n}\n\nexport function fail(code: CodeValue, message: string): AdepHttpEnvelope {\n return envelope({ code, data: null, message }, HTTP_STATUS[code] ?? 400)\n}\n"
|
|
629
|
-
},
|
|
630
|
-
{
|
|
631
|
-
"path": "functions/_shared/route.ts",
|
|
632
|
-
"content": "/**\n * \u8DEF\u5F84\u5F52\u4E00\u5316\uFF1A\u5E73\u53F0\u7F51\u5173 / CLI dev \u4F20\u7ED9\u51FD\u6570\u7684 ctx.path \u662F\u5B8C\u6574 pathname\uFF08\u542B\n * functions_prefix /api\uFF0C\u89C1 AGENTS.md v1.57 \u8FB9\u754C\u8BF4\u660E\uFF09\u3002\u8FD9\u91CC\u5265\u6389\u524D\u7F00\uFF0C\u51FD\u6570\u5185\n * \u4E00\u5F8B\u6309\u300C\u524D\u7F00\u540E\u8DEF\u5F84\u300D\u5339\u914D\uFF08/events\u3001/auth/login \u7B49\uFF09\u3002\n */\nexport function routePath(ctxPath: string): string {\n if (ctxPath === '/api') return '/'\n if (ctxPath.startsWith('/api/')) return ctxPath.slice(4)\n return ctxPath\n}\n"
|
|
624
|
+
"content": "/**\n * \u670D\u52A1\u7AEF\u65E5\u671F\u5DE5\u5177\uFF08\u79FB\u690D\u81EA cf-backend src/lib/date.ts\uFF09\u3002\n * \u4E0E\u5C0F\u7A0B\u5E8F utils/date.js \u7B97\u6CD5\u5BF9\u9F50\uFF1A\u5929\u6570\u6309\u300C\u5317\u4EAC\u65F6\u95F4\uFF08UTC+8\uFF09\u81EA\u7136\u65E5\u300D\u8BA1\u7B97\u3002\n */\nimport type { EventRow } from './store'\n\nconst WEEK = ['\u5468\u65E5', '\u5468\u4E00', '\u5468\u4E8C', '\u5468\u4E09', '\u5468\u56DB', '\u5468\u4E94', '\u5468\u516D']\n\n/** \u5317\u4EAC\u65F6\u95F4\u4ECA\u5929 YYYY-MM-DD\uFF08\u81EA\u7136\u65E5\uFF0C\u4E0D\u4EE5 UTC \u65E5\u671F\u4E3A\u51C6\uFF09 */\nexport function todayStr(): string {\n const now = new Date(Date.now() + 8 * 3600_000)\n const y = now.getUTCFullYear()\n const m = now.getUTCMonth() + 1\n const d = now.getUTCDate()\n return fmt(y, m, d)\n}\n\nfunction fmt(y: number, m: number, d: number): string {\n return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`\n}\n\nexport function isValidDate(s: unknown): s is string {\n if (typeof s !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return false\n const [y, m, d] = s.split('-').map(Number)\n if (m < 1 || m > 12 || d < 1 || d > 31) return false\n const dt = new Date(y, m - 1, d)\n return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d\n}\n\n/** \u6309\u5317\u4EAC\u65F6\u95F4\u81EA\u7136\u65E5\u5DEE\uFF08\u6B63\u6570\u672A\u6765 / \u8D1F\u6570\u8FC7\u53BB\uFF1Bcountup \u53D6\u7EDD\u5BF9\u503C\uFF09 */\nexport function calcDays(\n targetDate: string,\n direction: 'countdown' | 'countup' = 'countdown'\n): number {\n if (!isValidDate(targetDate)) return 0\n const target = new Date(`${targetDate}T00:00:00+08:00`).getTime()\n const now = new Date()\n const nowBeijing = new Date(now.getTime() + 8 * 3600_000)\n const todayStart = new Date(\n Date.UTC(nowBeijing.getUTCFullYear(), nowBeijing.getUTCMonth(), nowBeijing.getUTCDate()) -\n 8 * 3600_000\n ).getTime()\n const diff = Math.round((target - todayStart) / 86400000)\n return direction === 'countup' ? Math.abs(diff) : diff\n}\n\nexport function weekdayOf(targetDate: string): string {\n if (!isValidDate(targetDate)) return ''\n const [y, m, d] = targetDate.split('-').map(Number)\n return WEEK[new Date(y, m - 1, d).getDay()]\n}\n\n/** \u670D\u52A1\u5668\u65F6\u95F4 ISO\uFF08UTC\uFF0C\u5B58\u50A8\u7528\uFF09 */\nexport function nowIso(): string {\n return new Date().toISOString()\n}\n\n/** \u4E8B\u4EF6\u884C \u2192 DTO\uFF08\u9644 days / weekday\uFF1Bis_pinned \u5E03\u5C14\u5316\uFF09\u3002EventRow \u7C7B\u578B\u5728 store.ts\uFF08FN-029 \u53BB\u91CD\uFF09\u3002 */\nexport interface EventDTO {\n id: string\n title: string\n target_date: string\n note: string\n category: string\n direction: 'countdown' | 'countup'\n is_pinned: boolean\n sort_order: number\n created_at: string\n updated_at: string\n days: number\n}\n\nexport function toDTO(row: EventRow): EventDTO {\n const direction = row.direction === 'countup' ? 'countup' : 'countdown'\n const dto: EventDTO = {\n id: row.id,\n title: row.title,\n target_date: row.target_date,\n note: row.note ?? '',\n category: row.category ?? 'other',\n direction,\n is_pinned: !!row.is_pinned,\n sort_order: row.sort_order ?? 0,\n created_at: row.created_at ?? '',\n updated_at: row.updated_at ?? '',\n days: 0,\n }\n dto.days = calcDays(row.target_date, direction)\n return dto\n}\n\n/** \u5217\u8868\u6392\u5E8F\uFF1A\u7F6E\u9876\u5728\u524D\uFF0C\u5176\u4F59\u6309\u5269\u4F59\u5929\u6570\u5347\u5E8F\uFF08\u8FC7\u53BB\u7684\u5929\u6570\u6392\u5728\u6700\u540E\uFF09 */\nexport function sortEvents<T extends EventDTO>(events: T[]): T[] {\n return events.toSorted((a, b) => {\n if (a.is_pinned !== b.is_pinned) return a.is_pinned ? -1 : 1\n const da = a.days >= 0 ? a.days : Number.MAX_SAFE_INTEGER\n const db = b.days >= 0 ? b.days : Number.MAX_SAFE_INTEGER\n return da - db\n })\n}\n"
|
|
633
625
|
},
|
|
634
626
|
{
|
|
635
627
|
"path": "functions/_shared/store.ts",
|
|
636
|
-
"content": "/**\n * \u6570\u636E\u8BBF\u95EE\u5C42\uFF08\u79FB\u690D\u81EA cf-backend src/db/queries.ts + src/db/client.ts\uFF09\u3002\n *\n * \u8FC1\u79FB\u70B9\uFF1ACloudflare D1 \u7684 prepare().bind().run()/all() \u2192 adep `cloud.db` \u94FE\u5F0F\n * builder\uFF08\u7B49\u4EF7 SQLite \u5B50\u96C6\uFF1Bsim \u5F15\u64CE\u4E0E\u5E73\u53F0\u9879\u76EE\u5E93\u540C\u4E00\u5957\u5B9E\u73B0\uFF0C\u89C1\n * `packages/runtime/src/database/builder`\uFF09\u3002\u8868\u7ED3\u6784\u7531 functions/schema.sql \u58F0\u660E\uFF0C\n * \u90E8\u7F72\u524D\u7528 `adep db migrate` \u5EFA\u8868\uFF1B\u672C\u5730 dev\uFF08adep dev / vite \u63D2\u4EF6\uFF09\u7684 sim \u5F15\u64CE\n * \u8BBF\u95EE\u65F6\u4F1A\u81EA\u52A8\u5EFA\u8868\u3002\n */\nimport type { CloudDb, SqlValue } from '@adep/types'\n\n/** users \u8868\u884C\uFF08\u8BFB\u51FA\u7684\u884C\u5B57\u6BB5\u53EF\u7A7A\uFF0C\u5199\u65F6\u8865\u5168\uFF09 */\nexport interface UserRow {\n id: string\n device_id: string\n nickname: string | null\n avatar_url: string | null\n created_at: string | null\n updated_at: string | null\n}\n\n/** events \u8868\u884C\uFF08\u8BFB\u51FA\u7684\u884C\u5B57\u6BB5\u53EF\u7A7A\uFF0C\u5199\u65F6\u8865\u5168\uFF09 */\nexport interface EventRow {\n id: string\n user_id: string\n title: string\n target_date: string\n note: string | null\n category: string | null\n direction: 'countdown' | 'countup' | null\n is_pinned: number | boolean | null\n sort_order: number | null\n created_at: string | null\n updated_at: string | null\n}\n\n/** \u884C\u5BF9\u8C61 \u2192 builder \u53EF\u63A5\u53D7\u7684 SqlValue \u8BB0\u5F55\uFF08\u7ED3\u6784\u517C\u5BB9\u5373\u53EF\uFF0C\u5185\u90E8\u90FD\u662F\u57FA\u672C\u7C7B\u578B\uFF09\u3002 */\nfunction sql(row: Record<string, unknown>): Record<string, SqlValue> {\n return row as unknown as Record<string, SqlValue>\n}\n\n/** \u65B0\u5EFA\u7528\u6237\u884C\u7684\u5B8C\u6574\u5F62\u72B6\uFF08\u5199\u5165\u4E13\u7528\uFF09\u3002 */\nexport interface NewUserRow {\n id: string\n device_id: string\n nickname: string\n avatar_url: string\n created_at: string\n updated_at: string\n}\n\n/** \u65B0\u5EFA\u4E8B\u4EF6\u884C\u7684\u5B8C\u6574\u5F62\u72B6\uFF08\u5199\u5165\u4E13\u7528\uFF09\u3002 */\nexport interface NewEventRow {\n id: string\n user_id: string\n title: string\n target_date: string\n note: string\n category: string\n direction: 'countdown' | 'countup'\n is_pinned: 0 | 1\n sort_order: number\n created_at: string\n updated_at: string\n}\n\n// ---------- users ----------\n\nexport async function findUserByDeviceId(db: CloudDb, deviceId: string): Promise<UserRow | null> {\n
|
|
628
|
+
"content": "/**\n * \u6570\u636E\u8BBF\u95EE\u5C42\uFF08\u79FB\u690D\u81EA cf-backend src/db/queries.ts + src/db/client.ts\uFF09\u3002\n *\n * \u8FC1\u79FB\u70B9\uFF1ACloudflare D1 \u7684 prepare().bind().run()/all() \u2192 adep `cloud.db` \u94FE\u5F0F\n * builder\uFF08\u7B49\u4EF7 SQLite \u5B50\u96C6\uFF1Bsim \u5F15\u64CE\u4E0E\u5E73\u53F0\u9879\u76EE\u5E93\u540C\u4E00\u5957\u5B9E\u73B0\uFF0C\u89C1\n * `packages/runtime/src/database/builder`\uFF09\u3002\u8868\u7ED3\u6784\u7531 functions/schema.sql \u58F0\u660E\uFF0C\n * \u90E8\u7F72\u524D\u7528 `adep db migrate` \u5EFA\u8868\uFF1B\u672C\u5730 dev\uFF08adep dev / vite \u63D2\u4EF6\uFF09\u7684 sim \u5F15\u64CE\n * \u8BBF\u95EE\u65F6\u4F1A\u81EA\u52A8\u5EFA\u8868\u3002\n *\n * \u7C7B\u578B\u7EA6\u5B9A\uFF1A`cloud.db` \u7684\u884C\u5217\u7C7B\u578B\u7F16\u8BD1\u671F\u672A\u77E5\uFF08\u8FD0\u884C\u65F6\u5EFA\u8868\uFF0C\u89C1 @adep/types/cloud-db\uFF09\uFF0C\n * \u8FD9\u91CC\u7528 `firstAs<T>` / `rowsAs<T>` \u4E24\u4E2A\u5C0F\u52A9\u624B\u628A\u300C\u53CC\u91CD\u65AD\u8A00\u300D\u6536\u655B\u5230\u4E00\u5904\u2014\u2014\n * \u884C\u7ED3\u6784\u53D8\u5316\u65F6\u53EA\u9700\u6539\u8FD9\u91CC\u7684 T \u4E0E NewXxxRow\uFF0C\u4E0D\u518D\u6563\u843D 5 \u5904 `as unknown as`\u3002\n */\nimport type { CloudDb, Row, SqlValue } from '@adep/types'\n\n/** users \u8868\u884C\uFF08\u8BFB\u51FA\u7684\u884C\u5B57\u6BB5\u53EF\u7A7A\uFF0C\u5199\u65F6\u8865\u5168\uFF09 */\nexport interface UserRow {\n id: string\n device_id: string\n nickname: string | null\n avatar_url: string | null\n created_at: string | null\n updated_at: string | null\n}\n\n/** events \u8868\u884C\uFF08\u8BFB\u51FA\u7684\u884C\u5B57\u6BB5\u53EF\u7A7A\uFF0C\u5199\u65F6\u8865\u5168\uFF09\u2014\u2014date.ts \u7684 toDTO \u4E5F\u5F15\u7528\u8FD9\u4E00\u4EFD\uFF08FN-029 \u53BB\u91CD\uFF09\u3002 */\nexport interface EventRow {\n id: string\n user_id: string\n title: string\n target_date: string\n note: string | null\n category: string | null\n direction: 'countdown' | 'countup' | null\n is_pinned: number | boolean | null\n sort_order: number | null\n created_at: string | null\n updated_at: string | null\n}\n\n/** \u884C\u5BF9\u8C61 \u2192 builder \u53EF\u63A5\u53D7\u7684 SqlValue \u8BB0\u5F55\uFF08\u7ED3\u6784\u517C\u5BB9\u5373\u53EF\uFF0C\u5185\u90E8\u90FD\u662F\u57FA\u672C\u7C7B\u578B\uFF09\u3002 */\nfunction sql(row: Record<string, unknown>): Record<string, SqlValue> {\n return row as unknown as Record<string, SqlValue>\n}\n\n/** \u65AD\u8A00\u52A9\u624B\uFF08\u6536\u655B\u5904\uFF09\uFF1A\u9996\u884C \u2192 T | null\u3002 */\nfunction firstAs<T>(row: Row | null): T | null {\n return (row as unknown as T | undefined) ?? null\n}\n\n/** \u65AD\u8A00\u52A9\u624B\uFF08\u6536\u655B\u5904\uFF09\uFF1A\u884C\u6570\u7EC4 \u2192 T[]\u3002 */\nfunction rowsAs<T>(rows: Row[]): T[] {\n return rows as unknown as T[]\n}\n\n/** \u65B0\u5EFA\u7528\u6237\u884C\u7684\u5B8C\u6574\u5F62\u72B6\uFF08\u5199\u5165\u4E13\u7528\uFF09\u3002 */\nexport interface NewUserRow {\n id: string\n device_id: string\n nickname: string\n avatar_url: string\n created_at: string\n updated_at: string\n}\n\n/** \u65B0\u5EFA\u4E8B\u4EF6\u884C\u7684\u5B8C\u6574\u5F62\u72B6\uFF08\u5199\u5165\u4E13\u7528\uFF09\u3002 */\nexport interface NewEventRow {\n id: string\n user_id: string\n title: string\n target_date: string\n note: string\n category: string\n direction: 'countdown' | 'countup'\n is_pinned: 0 | 1\n sort_order: number\n created_at: string\n updated_at: string\n}\n\n// ---------- users ----------\n\nexport async function findUserByDeviceId(db: CloudDb, deviceId: string): Promise<UserRow | null> {\n return firstAs<UserRow>(await db.table('users').where('device_id', deviceId).first())\n}\n\nexport async function findUserById(db: CloudDb, id: string): Promise<UserRow | null> {\n return firstAs<UserRow>(await db.table('users').where('id', id).first())\n}\n\nexport async function createUser(db: CloudDb, row: NewUserRow): Promise<void> {\n await db.table('users').insert(sql(row as unknown as Record<string, unknown>))\n}\n\n// ---------- events ----------\n\nexport async function listEvents(db: CloudDb, userId: string): Promise<EventRow[]> {\n return rowsAs<EventRow>(await db.table('events').where('user_id', userId).get())\n}\n\nexport async function findEventById(\n db: CloudDb,\n id: string,\n userId: string\n): Promise<EventRow | null> {\n return firstAs<EventRow>(\n await db.table('events').where('id', id).where('user_id', userId).first()\n )\n}\n\nexport async function countEvents(db: CloudDb, userId: string): Promise<number> {\n // cloud.db \u7684 count() \u8FD4\u56DE number\uFF08builder \u5185\u90E8\u53D6 SELECT count(*) AS n \u7684 n\uFF09\n const n = await db.table('events').where('user_id', userId).count()\n return typeof n === 'number' ? n : Number(n ?? 0)\n}\n\nexport async function insertEvent(db: CloudDb, row: NewEventRow): Promise<void> {\n await db.table('events').insert(sql(row as unknown as Record<string, unknown>))\n}\n\nexport async function updateEvent(\n db: CloudDb,\n id: string,\n userId: string,\n patch: Partial<NewEventRow>\n): Promise<void> {\n await db\n .table('events')\n .where('id', id)\n .where('user_id', userId)\n .update(sql(patch as unknown as Record<string, unknown>))\n}\n\nexport async function deleteEvent(db: CloudDb, id: string, userId: string): Promise<void> {\n await db.table('events').where('id', id).where('user_id', userId).delete()\n}\n"
|
|
637
629
|
},
|
|
638
630
|
{
|
|
639
631
|
"path": "functions/auth.ts",
|
|
640
|
-
"content": "/**\n * \u4E91\u51FD\u6570\uFF1Aauth \u2014\u2014 \u767B\u5F55 / \u5F53\u524D\u7528\u6237\uFF08\u79FB\u690D\u81EA cf-backend src/routes/auth.ts\uFF09\u3002\n *\n * \u8DEF\u7531\uFF08\
|
|
632
|
+
"content": "/**\n * \u4E91\u51FD\u6570\uFF1Aauth \u2014\u2014 \u767B\u5F55 / \u5F53\u524D\u7528\u6237\uFF08\u79FB\u690D\u81EA cf-backend src/routes/auth.ts\uFF09\u3002\n *\n * FN-029 \u8D77\u7528\u5B98\u65B9\u8DEF\u7531\u52A9\u624B\uFF08`@adep/runtime/route`\uFF09\u58F0\u660E\u5F0F\u6CE8\u518C\uFF1A\n * \u524D\u7F00\u5265\u79BB\u3001\u8DEF\u5F84\u53C2\u6570\u3001404/405\u3001\u9274\u6743\u4E2D\u95F4\u4EF6\u5168\u90E8\u7531\u8DEF\u7531\u5668\u627F\u62C5\uFF0C\u51FD\u6570\u4F53\u53EA\u5269\u4E1A\u52A1\u5206\u652F\u3002\n *\n * \u8DEF\u7531\uFF08\u5DF2\u5265 /api \u524D\u7F00\uFF09\uFF1A\n * POST /auth/login { deviceId } \u2192 \u53D6/\u5EFA\u7528\u6237\uFF0C\u8FD4\u56DE { token, expires_in, user }\n * GET /auth/me \u2192 \u5F53\u524D\u7528\u6237 + \u4E8B\u4EF6\u8BA1\u6570\n *\n * \u9274\u6743\u53CC\u6765\u6E90\uFF08\u767B\u5F55\u5373\u670D\u52A1\uFF0C\u89C1 _shared/auth.ts\uFF09\uFF1AOIDC \u8EAB\u4EFD\uFF08`Authorization: Bearer`\n * OAuth token\uFF0C\u7F51\u5173\u89E3\u6790\u6210 ctx.user\uFF09\u4F18\u5148\uFF1B\u672A\u547D\u4E2D\u56DE\u9000 device_id \u51ED\u636E\u3002`/auth/me` \u7684\n * `auth_mode` \u5B57\u6BB5\u636E\u6B64\u533A\u5206\uFF1A'oidc' = \u5E73\u53F0\u4E09\u65B9\u767B\u5F55 / 'device' = \u672C\u5730\u8BBE\u5907\u8EAB\u4EFD\u3002\n */\nimport type { FunctionContext } from '@adep/types'\nimport { createRouter } from '@adep/runtime/route'\nimport { ok, fail, CODE } from '@adep/runtime/response'\nimport {\n findUserByDeviceId,\n findUserById,\n createUser,\n countEvents,\n type NewUserRow,\n} from './_shared/store'\nimport { resolveUser, parseBearer } from './_shared/auth'\nimport { nowIso } from './_shared/date'\n\ninterface UserDTO {\n id: string\n nickname: string\n avatar_url: string\n created_at: string\n}\n\nfunction toUserDTO(row: {\n id: string\n nickname: string | null\n avatar_url: string | null\n created_at: string | null\n}): UserDTO {\n return {\n id: row.id,\n nickname: row.nickname ?? '\u672C\u5730\u7528\u6237',\n avatar_url: row.avatar_url ?? '',\n created_at: row.created_at ?? '',\n }\n}\n\nconst router = createRouter<FunctionContext>()\n\n// \u9274\u6743\u53E3\u5F84\uFF1Alogin \u662F\u300C\u6362\u51ED\u636E\u300D\u7AEF\u70B9\u3001me \u662F\u300C\u9A8C\u51ED\u636E\u300D\u7AEF\u70B9\u2014\u2014auth \u51FD\u6570\u4E0D\u6302\u5168\u5C40\u5B88\u536B\uFF0C\n// me \u5728 handler \u5185\u8D70 resolveUser\uFF08authGuard \u7684\u5B6A\u751F\u51FD\u6570\uFF09\uFF0C\u5931\u8D25\u8BED\u4E49\u4E0E\u5B88\u536B\u5B8C\u5168\u4E00\u81F4\u3002\nrouter.post('/auth/login', async (req) => {\n const ctx = req.raw\n const db = ctx.cloud.db\n const body = (ctx.body ?? {}) as Record<string, unknown>\n const deviceId = typeof body.deviceId === 'string' ? body.deviceId.trim() : ''\n if (!deviceId || deviceId.length < 8 || deviceId.length > 128) {\n return fail(CODE.BAD_PARAM, 'deviceId \u53C2\u6570\u4E0D\u5408\u6CD5')\n }\n\n try {\n let user = await findUserByDeviceId(db, deviceId)\n if (!user) {\n const fresh: NewUserRow = {\n id: crypto.randomUUID(),\n device_id: deviceId,\n nickname: '\u672C\u5730\u7528\u6237',\n avatar_url: '',\n created_at: nowIso(),\n updated_at: nowIso(),\n }\n await createUser(db, fresh)\n user = fresh\n }\n return ok({\n token: deviceId, // \u8BBE\u5907\u8EAB\u4EFD\u5373 Bearer \u51ED\u636E\uFF08\u7B49\u4EF7 API Key\uFF1B\u524D\u7AEF\u6301\u4E45\u5316\u4E8E localStorage\uFF09\n expires_in: 7 * 24 * 3600,\n user: toUserDTO(user),\n })\n } catch (err) {\n console.error('[auth/login]', err)\n return fail(CODE.SERVER_ERROR, '\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5')\n }\n})\n\nrouter.get('/auth/me', async (req) => {\n const ctx = req.raw\n const db = ctx.cloud.db\n const user = await resolveUser(db, ctx)\n if (user === null) {\n return fail(CODE.NO_AUTH, parseBearer(ctx) === '' ? '\u672A\u767B\u5F55' : '\u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55')\n }\n const eventCount = await countEvents(db, user.id)\n const fresh = (await findUserById(db, user.id)) ?? user\n return ok({\n user: toUserDTO(fresh),\n event_count: eventCount,\n max_events: 200,\n // \u767B\u5F55\u5373\u670D\u52A1\uFF1A'oidc' = \u5E73\u53F0\u4E09\u65B9\u767B\u5F55\uFF08\u7F51\u5173\u89E3\u6790 Bearer \u2192 ctx.user\uFF09\uFF1B'device' = \u672C\u5730\u8BBE\u5907\u8EAB\u4EFD\u3002\n auth_mode: ctx.user !== null ? 'oidc' : 'device',\n })\n})\n\nexport default async function handle(ctx: FunctionContext) {\n return router.handleAsEnvelope(ctx)\n}\n"
|
|
641
633
|
},
|
|
642
634
|
{
|
|
643
635
|
"path": "functions/events.ts",
|
|
644
|
-
"content": "/**\n * \u4E91\u51FD\u6570\uFF1Aevents \u2014\u2014 \u4E8B\u4EF6 CRUD\uFF08\u79FB\u690D\u81EA cf-backend src/routes/events.ts\uFF09\u3002\n *\n * \u8DEF\u7531\uFF08\
|
|
636
|
+
"content": "/**\n * \u4E91\u51FD\u6570\uFF1Aevents \u2014\u2014 \u4E8B\u4EF6 CRUD\uFF08\u79FB\u690D\u81EA cf-backend src/routes/events.ts\uFF09\u3002\n *\n * FN-029 \u8D77\u7528\u5B98\u65B9\u8DEF\u7531\u52A9\u624B\uFF08`@adep/runtime/route`\uFF09+ \u9274\u6743\u4E2D\u95F4\u4EF6\uFF08`_shared/auth.ts`\n * \u7684 authGuard\uFF09\uFF1A\u524D\u7F00\u5265\u79BB\u3001`:id` \u8DEF\u5F84\u53C2\u6570\u3001404/405\u3001\u6BCF\u7AEF\u70B9\u7684\u9274\u6743\u6536\u7A84\u6837\u677F\u5168\u90E8\n * \u6D88\u9664\u2014\u2014handler \u91CC `req.state.user` \u5373\u5DF2\u767B\u5F55\u7528\u6237\uFF0C\u51FD\u6570\u4F53\u53EA\u5269\u7EAF\u4E1A\u52A1\u903B\u8F91\u3002\n *\n * \u8DEF\u7531\uFF08\u5DF2\u5265 /api \u524D\u7F00\uFF09\uFF1A\n * GET /events?category=xxx \u2192 { events }\uFF08\u7F6E\u9876\u4F18\u5148\uFF0C\u5176\u4F59\u6309\u5269\u4F59\u5929\u6570\u5347\u5E8F\uFF09\n * POST /events \u2192 \u521B\u5EFA\uFF08\u4E0A\u9650 200 \u4E2A\uFF09\n * GET /events/:id \u2192 { event }\n * PUT /events/:id \u2192 \u5C40\u90E8\u66F4\u65B0\n * DELETE /events/:id \u2192 \u5220\u9664\n */\nimport type { FunctionContext } from '@adep/types'\nimport { createRouter } from '@adep/runtime/route'\nimport { ok, fail, CODE } from '@adep/runtime/response'\nimport { authGuard } from './_shared/auth'\nimport type { UserRow } from './_shared/store'\nimport { isValidDate, toDTO, sortEvents, nowIso } from './_shared/date'\nimport type { EventRow } from './_shared/store'\nimport {\n findEventById,\n insertEvent,\n updateEvent,\n deleteEvent,\n listEvents,\n countEvents,\n type NewEventRow,\n} from './_shared/store'\n\nconst CATEGORIES = new Set(['other', 'birthday', 'anniversary', 'exam', 'holiday'])\nconst MAX_EVENTS = 200\n\n/** \u4ECE\u4E2D\u95F4\u4EF6\u80CC\u5305\u53D6\u5DF2\u767B\u5F55\u7528\u6237\uFF08authGuard \u4FDD\u8BC1\u5B58\u5728\uFF1B\u7C7B\u578B\u65AD\u8A00\u53EA\u5728\u8FD9\u4E00\u5904\uFF09\u3002 */\nfunction userOf(req: { state: Record<string, unknown> }): UserRow {\n return req.state.user as UserRow\n}\n\nconst router = createRouter<FunctionContext>()\n\n// \u5168\u51FD\u6570\u9274\u6743\uFF1A\u6CE8\u518C\u4E4B\u540E\u7684\u6240\u6709\u7AEF\u70B9\u90FD\u8981\u6C42\u767B\u5F55\uFF08401 \u77ED\u8DEF\uFF0C\u4E0D\u8FDB handler\uFF09\u3002\nrouter.use(authGuard())\n\n// GET /events \u2014\u2014 \u5217\u8868\uFF08\u53EF\u6309\u5206\u7C7B\u8FC7\u6EE4\uFF1B\u7F6E\u9876\u5728\u524D\uFF0C\u5176\u4F59\u6309\u5269\u4F59\u5929\u6570\u5347\u5E8F\uFF09\nrouter.get('/events', async (req) => {\n const user = userOf(req)\n const ctx = req.raw\n const db = ctx.cloud.db\n const category = ctx.query?.category\n let events = (await listEvents(db, user.id)).map((row) => toDTO(row as EventRow))\n if (typeof category === 'string' && category && CATEGORIES.has(category)) {\n events = events.filter((e) => e.category === category)\n }\n return ok({ events: sortEvents(events) })\n})\n\n// POST /events \u2014\u2014 \u521B\u5EFA\nrouter.post('/events', async (req) => {\n const user = userOf(req)\n const ctx = req.raw\n const db = ctx.cloud.db\n const body = (ctx.body ?? {}) as Record<string, unknown>\n\n const title = typeof body.title === 'string' ? body.title.trim() : ''\n const target_date = typeof body.target_date === 'string' ? body.target_date : ''\n const note = typeof body.note === 'string' ? body.note.trim() : ''\n const category = typeof body.category === 'string' ? body.category : 'other'\n const direction = body.direction === 'countup' ? 'countup' : 'countdown'\n const is_pinned = !!body.is_pinned\n\n if (!title) return fail(CODE.BAD_PARAM, '\u8BF7\u586B\u5199\u4E8B\u4EF6\u540D\u79F0')\n if (title.length > 50) return fail(CODE.BAD_PARAM, '\u540D\u79F0\u4E0D\u80FD\u8D85\u8FC7 50 \u4E2A\u5B57')\n if (!isValidDate(target_date)) return fail(CODE.BAD_PARAM, '\u8BF7\u9009\u62E9\u76EE\u6807\u65E5\u671F')\n if (note.length > 200) return fail(CODE.BAD_PARAM, '\u5907\u6CE8\u4E0D\u80FD\u8D85\u8FC7 200 \u4E2A\u5B57')\n if (!CATEGORIES.has(category)) return fail(CODE.BAD_PARAM, '\u5206\u7C7B\u4E0D\u5408\u6CD5')\n\n const count = await countEvents(db, user.id)\n if (count >= MAX_EVENTS) return fail(CODE.LIMIT_EXCEEDED, `\u6700\u591A\u521B\u5EFA ${MAX_EVENTS} \u4E2A\u4E8B\u4EF6`)\n\n const now = nowIso()\n const event: NewEventRow = {\n id: crypto.randomUUID(),\n user_id: user.id,\n title,\n target_date,\n note,\n category,\n direction,\n is_pinned: is_pinned ? 1 : 0,\n sort_order: 0,\n created_at: now,\n updated_at: now,\n }\n await insertEvent(db, event)\n return ok({ event: toDTO(event) }, '\u521B\u5EFA\u6210\u529F')\n})\n\n// GET /events/:id \u2014\u2014 \u8BE6\u60C5\nrouter.get('/events/:id', async (req) => {\n const user = userOf(req)\n const db = req.raw.cloud.db\n const event = await findEventById(db, req.params.id ?? '', user.id)\n if (!event) return fail(CODE.NOT_FOUND, '\u4E8B\u4EF6\u4E0D\u5B58\u5728')\n return ok({ event: toDTO(event as EventRow) })\n})\n\n// PUT /events/:id \u2014\u2014 \u5C40\u90E8\u66F4\u65B0\uFF08\u53EA\u6536\u51FA\u73B0\u7684\u5B57\u6BB5\uFF09\nrouter.put('/events/:id', async (req) => {\n const user = userOf(req)\n const ctx = req.raw\n const db = ctx.cloud.db\n const event = await findEventById(db, req.params.id ?? '', user.id)\n if (!event) return fail(CODE.NOT_FOUND, '\u4E8B\u4EF6\u4E0D\u5B58\u5728')\n\n const body = (ctx.body ?? {}) as Record<string, unknown>\n const patch: Partial<NewEventRow> = {}\n const { title, target_date, note, category, direction, is_pinned } = body\n if (title !== undefined) {\n if (typeof title !== 'string' || !title.trim()) return fail(CODE.BAD_PARAM, '\u8BF7\u586B\u5199\u4E8B\u4EF6\u540D\u79F0')\n if (title.trim().length > 50) return fail(CODE.BAD_PARAM, '\u540D\u79F0\u4E0D\u80FD\u8D85\u8FC7 50 \u4E2A\u5B57')\n patch.title = title.trim()\n }\n if (target_date !== undefined) {\n if (typeof target_date !== 'string' || !isValidDate(target_date)) {\n return fail(CODE.BAD_PARAM, '\u8BF7\u9009\u62E9\u76EE\u6807\u65E5\u671F')\n }\n patch.target_date = target_date\n }\n if (note !== undefined) {\n if (typeof note !== 'string') return fail(CODE.BAD_PARAM, '\u5907\u6CE8\u683C\u5F0F\u4E0D\u5408\u6CD5')\n if (note.trim().length > 200) return fail(CODE.BAD_PARAM, '\u5907\u6CE8\u4E0D\u80FD\u8D85\u8FC7 200 \u4E2A\u5B57')\n patch.note = note.trim()\n }\n if (category !== undefined) {\n if (typeof category !== 'string' || !CATEGORIES.has(category)) {\n return fail(CODE.BAD_PARAM, '\u5206\u7C7B\u4E0D\u5408\u6CD5')\n }\n patch.category = category\n }\n if (direction !== undefined) {\n if (direction !== 'countdown' && direction !== 'countup') {\n return fail(CODE.BAD_PARAM, '\u8BA1\u65F6\u65B9\u5F0F\u4E0D\u5408\u6CD5')\n }\n patch.direction = direction\n }\n if (is_pinned !== undefined) {\n patch.is_pinned = is_pinned ? 1 : 0\n }\n\n patch.updated_at = nowIso()\n await updateEvent(db, event.id, user.id, patch)\n const fresh = await findEventById(db, event.id, user.id)\n return ok({ event: toDTO((fresh ?? event) as EventRow) }, '\u5DF2\u4FDD\u5B58')\n})\n\n// DELETE /events/:id \u2014\u2014 \u5220\u9664\nrouter.delete('/events/:id', async (req) => {\n const user = userOf(req)\n const db = req.raw.cloud.db\n const event = await findEventById(db, req.params.id ?? '', user.id)\n if (!event) return fail(CODE.NOT_FOUND, '\u4E8B\u4EF6\u4E0D\u5B58\u5728')\n await deleteEvent(db, event.id, user.id)\n return ok({ id: event.id }, '\u5DF2\u5220\u9664')\n})\n\nexport default async function handle(ctx: FunctionContext) {\n return router.handleAsEnvelope(ctx)\n}\n"
|
|
645
637
|
},
|
|
646
638
|
{
|
|
647
639
|
"path": "web/src/App.vue",
|
|
@@ -697,7 +689,7 @@ declare module '*.vue' {
|
|
|
697
689
|
},
|
|
698
690
|
{
|
|
699
691
|
"path": "web/src/lib/events.test.ts",
|
|
700
|
-
"content": "/**\n * countdown web \u4E8B\u4EF6\u6570\u636E\u5C42\u5355\u6D4B\uFF08\u767B\u5F55\u5373\u670D\u52A1\uFF09\uFF1A\u5DF2\u767B\u5F55 \u2192 \u4E91\u7AEF /api/events\uFF1B\n * \u672A\u767B\u5F55 \u2192 localStorage\u3002\u8986\u76D6\u5217\u8868 / \u589E\u5220\u6539 / \u767B\u5F55\u540E\u540C\u6B65\uFF08syncLocalEvents\uFF09\u3002\n * \u8FD0\u884C\
|
|
692
|
+
"content": "/**\n * countdown web \u4E8B\u4EF6\u6570\u636E\u5C42\u5355\u6D4B\uFF08\u767B\u5F55\u5373\u670D\u52A1\uFF09\uFF1A\u5DF2\u767B\u5F55 \u2192 \u4E91\u7AEF /api/events\uFF1B\n * \u672A\u767B\u5F55 \u2192 localStorage\u3002\u8986\u76D6\u5217\u8868 / \u589E\u5220\u6539 / \u767B\u5F55\u540E\u540C\u6B65\uFF08syncLocalEvents\uFF09\u3002\n * \u8FD0\u884C\uFF1A\u9879\u76EE\u6839\u4E0B `npm run test`\uFF08app \u7EA7 vitest \u6536\u96C6 web \u7684\u6D4B\u8BD5\u6587\u4EF6\uFF09\u3002\n */\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\nimport { request } from './request'\nimport type { CountdownEvent } from './date'\n\n// ---- mock \u4F9D\u8D56\uFF1Aoidc\uFF08\u767B\u5F55\u6001\u5F00\u5173\uFF09\u4E0E request\uFF08\u4E91\u7AEF\u54CD\u5E94\uFF09----\n\nconst mocks = vi.hoisted(() => ({\n loggedIn: false as boolean,\n}))\n\nvi.mock('./oidc', () => ({\n hasSession: () => mocks.loggedIn,\n getRequestToken: async () => (mocks.loggedIn ? 'tok' : ''),\n forceRefreshToken: async () => null,\n hasSessionExport: undefined,\n loadSession: () => null,\n}))\n\nvi.mock('./request', () => ({\n request: vi.fn(),\n}))\n\nimport { listEvents, createEvent, updateEvent, deleteEvent, syncLocalEvents, isLoggedIn } from './events'\n\nconst LOCAL_KEY = 'countdown_local_events'\n\n// ---- node \u73AF\u5883\u65E0 localStorage\uFF1A\u88C5\u4E00\u4E2A\u5185\u5B58\u6869\uFF08data \u5C42\u53EA\u5728\u8C03\u7528\u65F6\u8BFB\u5199\uFF09----\n\nclass MemoryStorage {\n private store = new Map<string, string>()\n getItem(key: string): string | null {\n return this.store.get(key) ?? null\n }\n setItem(key: string, value: string): void {\n this.store.set(key, value)\n }\n removeItem(key: string): void {\n this.store.delete(key)\n }\n clear(): void {\n this.store.clear()\n }\n get length(): number {\n return this.store.size\n }\n key(index: number): string | null {\n return [...this.store.keys()][index] ?? null\n }\n}\n\n// \u53EA\u5728\u672A\u5B9A\u4E49\u65F6\u88C5\uFF08\u6D4F\u89C8\u5668\u73AF\u5883 / jsdom \u76F4\u63A5\u590D\u7528\uFF09\u3002\nconst g = globalThis as Record<string, unknown>\nif (typeof g.localStorage !== 'object' || g.localStorage === null) {\n Object.defineProperty(g, 'localStorage', { value: new MemoryStorage(), writable: true })\n}\n\n/** \u8BFB localStorage \u91CC\u7684\u672C\u5730\u4E8B\u4EF6\u3002 */\nfunction readStored(): CountdownEvent[] {\n return JSON.parse(localStorage.getItem(LOCAL_KEY) ?? '[]') as CountdownEvent[]\n}\n\n/** \u624B\u5DE5\u6784\u9020\u4E00\u6761\u4E91\u7AEF\u4E8B\u4EF6\u3002 */\nfunction cloudEvent(overrides: Partial<CountdownEvent> = {}): CountdownEvent {\n return {\n id: 'c-1',\n title: '\u4E91\u7AEF\u4E8B\u4EF6',\n target_date: '2026-12-31',\n note: '',\n category: 'other',\n direction: 'countdown',\n is_pinned: false,\n sort_order: 0,\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n days: 0,\n ...overrides,\n }\n}\n\n/** \u9020\u4E00\u6761\u5DF2\u5B58\u5728\u7684\u672C\u5730\u4E8B\u4EF6\uFF08\u76F4\u63A5\u843D localStorage\uFF09\u3002 */\nfunction seedLocal(event: CountdownEvent): void {\n localStorage.setItem(LOCAL_KEY, JSON.stringify([event]))\n}\n\nconst localEvent = (): CountdownEvent =>\n cloudEvent({\n id: 'local-1',\n title: '\u672C\u5730\u4E8B\u4EF6',\n target_date: '2026-10-01',\n created_at: '2026-02-01T00:00:00.000Z',\n updated_at: '2026-02-01T00:00:00.000Z',\n })\n\nbeforeEach(() => {\n mocks.loggedIn = false\n localStorage.clear()\n vi.clearAllMocks()\n})\n\nafterEach(() => {\n localStorage.clear()\n})\n\ndescribe('\u767B\u5F55\u6001\u5224\u5B9A', () => {\n it('isLoggedIn \u8DDF\u968F OIDC \u4F1A\u8BDD', () => {\n mocks.loggedIn = false\n expect(isLoggedIn()).toBe(false)\n mocks.loggedIn = true\n expect(isLoggedIn()).toBe(true)\n })\n})\n\ndescribe('\u672A\u767B\u5F55\uFF1A\u5168\u90E8\u843D localStorage', () => {\n it('listEvents \u8BFB\u672C\u5730\uFF08\u4E0D\u8BF7\u6C42\u4E91\u7AEF\uFF09', async () => {\n seedLocal(localEvent())\n const list = await listEvents()\n expect(list).toHaveLength(1)\n expect(list[0]?.id).toBe('local-1')\n expect(request).not.toHaveBeenCalled()\n })\n\n it('createEvent \u843D\u672C\u5730\u5E76\u8865\u5168\u5B57\u6BB5', async () => {\n const created = await createEvent({ title: '\u65B0\u4E8B\u4EF6', target_date: '2026-11-11' })\n expect(created.id).toBeTypeOf('string')\n expect(readStored()).toHaveLength(1)\n expect(readStored()[0]?.title).toBe('\u65B0\u4E8B\u4EF6')\n expect(request).not.toHaveBeenCalled()\n })\n\n it('updateEvent / deleteEvent \u64CD\u4F5C\u672C\u5730\u884C', async () => {\n seedLocal(localEvent())\n const updated = await updateEvent('local-1', { title: '\u6539\u540D' })\n expect(updated.title).toBe('\u6539\u540D')\n expect(readStored()[0]?.title).toBe('\u6539\u540D')\n\n await deleteEvent('local-1')\n expect(readStored()).toHaveLength(0)\n expect(request).not.toHaveBeenCalled()\n })\n})\n\ndescribe('\u5DF2\u767B\u5F55\uFF1A\u8D70\u4E91\u7AEF', () => {\n beforeEach(() => {\n mocks.loggedIn = true\n })\n\n it('listEvents \u62C9\u4E91\u7AEF\u5E76\u8FD4\u56DE events \u6570\u7EC4', async () => {\n vi.mocked(request).mockResolvedValue({ events: [cloudEvent()] })\n const list = await listEvents()\n expect(list).toHaveLength(1)\n expect(list[0]?.id).toBe('c-1')\n expect(request).toHaveBeenCalledWith('/api/events')\n })\n\n it('\u4E91\u7AEF\u62C9\u53D6\u5931\u8D25 \u2192 \u56DE\u9000\u672C\u5730\u7F13\u5B58\uFF08\u4E0D\u629B\u9519\uFF09', async () => {\n seedLocal(localEvent())\n vi.mocked(request).mockRejectedValue(new Error('\u7F51\u7EDC\u5F02\u5E38'))\n const list = await listEvents()\n expect(list).toHaveLength(1)\n expect(list[0]?.id).toBe('local-1')\n })\n\n it('createEvent \u8C03\u4E91\u7AEF POST', async () => {\n vi.mocked(request).mockResolvedValue({ event: cloudEvent() })\n await createEvent({ title: 'x', target_date: '2026-12-01' })\n expect(request).toHaveBeenCalledWith(\n '/api/events',\n expect.objectContaining({ method: 'POST', data: { title: 'x', target_date: '2026-12-01' } })\n )\n })\n\n it('deleteEvent \u8C03\u4E91\u7AEF DELETE', async () => {\n vi.mocked(request).mockResolvedValue(null)\n await deleteEvent('c-1')\n expect(request).toHaveBeenCalledWith('/api/events/c-1', expect.objectContaining({ method: 'DELETE' }))\n })\n})\n\ndescribe('syncLocalEvents\uFF1A\u767B\u5F55\u540E\u628A\u672C\u5730\u5E76\u5165\u4E91\u7AEF', () => {\n it('\u672A\u767B\u5F55 \u2192 0 \u4E14\u4E0D\u8BF7\u6C42', async () => {\n mocks.loggedIn = false\n seedLocal(localEvent())\n expect(await syncLocalEvents()).toBe(0)\n expect(request).not.toHaveBeenCalled()\n })\n\n it('\u5DF2\u767B\u5F55\uFF1A\u672C\u5730\u5168\u90E8\u4E0A\u4F20 \u2192 \u8FD4\u56DE\u6761\u6570\u5E76\u6E05\u7A7A\u672C\u5730', async () => {\n mocks.loggedIn = true\n seedLocal(localEvent())\n\n // \u4E91\u7AEF\u4E3A\u7A7A\uFF1A\u6BCF\u4E2A\u672C\u5730\u4E8B\u4EF6\u90FD\u65B0\u589E\uFF08request \u4F9D\u6B21\u5E94\u7B54\u5217\u8868\u4E0E\u6BCF\u6B21 POST\uFF09\u3002\n vi.mocked(request)\n .mockResolvedValueOnce({ events: [] })\n .mockResolvedValueOnce({ event: cloudEvent({ id: 'synced-1' }) })\n\n const n = await syncLocalEvents()\n expect(n).toBe(1)\n expect(readStored()).toHaveLength(0)\n expect(request).toHaveBeenCalledWith('/api/events', { retry: false })\n expect(request).toHaveBeenCalledWith(\n '/api/events',\n expect.objectContaining({ method: 'POST', data: expect.objectContaining({ title: '\u672C\u5730\u4E8B\u4EF6' }) })\n )\n })\n\n it('\u5DF2\u767B\u5F55\uFF1A\u4E0E\u4E91\u7AEF\u91CD\u590D\u7684\u4E8B\u4EF6\u8DF3\u8FC7\u4E0A\u4F20\uFF08\u6309 title+target_date \u53BB\u91CD\uFF09', async () => {\n mocks.loggedIn = true\n const dup = cloudEvent({ id: 'local-dup', title: '\u672C\u5730\u4E8B\u4EF6', target_date: '2026-10-01' })\n seedLocal(dup)\n\n // \u4E91\u7AEF\u5DF2\u6709\u540C title+target_date \u7684\u4E8B\u4EF6\u3002\n vi.mocked(request).mockResolvedValue({\n events: [cloudEvent({ id: 'c-dup', title: '\u672C\u5730\u4E8B\u4EF6', target_date: '2026-10-01' })],\n })\n\n const n = await syncLocalEvents()\n expect(n).toBe(0)\n // \u4ECD\u89C6\u4E3A\u5168\u90E8\u5E76\u5165 \u2192 \u6E05\u7A7A\u672C\u5730\uFF08\u91CD\u590D\u7684\u4E91\u7AEF\u5DF2\u5B58\u5728\uFF09\u3002\n expect(readStored()).toHaveLength(0)\n })\n})\n"
|
|
701
693
|
},
|
|
702
694
|
{
|
|
703
695
|
"path": "web/src/lib/events.ts",
|
|
@@ -3297,6 +3289,57 @@ function asCfBindingsConfig(value, errors) {
|
|
|
3297
3289
|
if (r2 !== null && r2 !== void 0) bindings.r2 = r2;
|
|
3298
3290
|
return bindings;
|
|
3299
3291
|
}
|
|
3292
|
+
function asTriggersConfig(value, errors) {
|
|
3293
|
+
if (!Array.isArray(value)) {
|
|
3294
|
+
errors.push(`triggers \u5FC5\u987B\u662F\u6570\u7EC4\uFF0C\u6536\u5230 ${typeNameOf(value)}`);
|
|
3295
|
+
return null;
|
|
3296
|
+
}
|
|
3297
|
+
const out = [];
|
|
3298
|
+
value.forEach((item, i) => {
|
|
3299
|
+
const idx = `triggers[${i}]`;
|
|
3300
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
3301
|
+
errors.push(`${idx} \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u6536\u5230 ${typeNameOf(item)}`);
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
const rec = item;
|
|
3305
|
+
if (typeof rec.function !== "string" || rec.function.length === 0) {
|
|
3306
|
+
errors.push(`${idx}.function \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF0C\u6536\u5230 ${typeNameOf(rec.function)}`);
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
3309
|
+
if (rec.type !== "cron" && rec.type !== "storage.upload") {
|
|
3310
|
+
errors.push(`${idx}.type \u5FC5\u987B\u662F 'cron' \u6216 'storage.upload'\uFF0C\u6536\u5230 ${typeNameOf(rec.type)}`);
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
const entry = { function: rec.function, type: rec.type };
|
|
3314
|
+
if (rec.cron !== void 0) {
|
|
3315
|
+
if (typeof rec.cron !== "string") {
|
|
3316
|
+
errors.push(`${idx}.cron \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u6536\u5230 ${typeNameOf(rec.cron)}`);
|
|
3317
|
+
} else if (rec.type === "cron" && rec.cron.trim().length === 0) {
|
|
3318
|
+
errors.push(`${idx}.cron \u4E0D\u80FD\u4E3A\u7A7A\uFF08type='cron' \u5FC5\u987B\u7ED9\u51FA 5 \u5B57\u6BB5 cron \u8868\u8FBE\u5F0F\uFF09`);
|
|
3319
|
+
} else {
|
|
3320
|
+
entry.cron = rec.cron;
|
|
3321
|
+
}
|
|
3322
|
+
} else if (rec.type === "cron") {
|
|
3323
|
+
errors.push(`${idx}.cron \u7F3A\u5931\uFF1Atype='cron' \u5FC5\u987B\u7ED9\u51FA 5 \u5B57\u6BB5 cron \u8868\u8FBE\u5F0F`);
|
|
3324
|
+
}
|
|
3325
|
+
if (rec.prefix !== void 0) {
|
|
3326
|
+
if (typeof rec.prefix !== "string") {
|
|
3327
|
+
errors.push(`${idx}.prefix \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u6536\u5230 ${typeNameOf(rec.prefix)}`);
|
|
3328
|
+
} else {
|
|
3329
|
+
entry.prefix = rec.prefix;
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
if (rec.enabled !== void 0) {
|
|
3333
|
+
if (typeof rec.enabled !== "boolean") {
|
|
3334
|
+
errors.push(`${idx}.enabled \u5FC5\u987B\u662F\u5E03\u5C14\u503C\uFF0C\u6536\u5230 ${typeNameOf(rec.enabled)}`);
|
|
3335
|
+
} else {
|
|
3336
|
+
entry.enabled = rec.enabled;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
out.push(entry);
|
|
3340
|
+
});
|
|
3341
|
+
return out;
|
|
3342
|
+
}
|
|
3300
3343
|
function parseAdepConfig(raw) {
|
|
3301
3344
|
if (raw === null || raw === void 0) {
|
|
3302
3345
|
return { ok: true, data: { ...DEFAULT_ADEP_CONFIG } };
|
|
@@ -3361,6 +3404,10 @@ function parseAdepConfig(raw) {
|
|
|
3361
3404
|
const bindings = asCfBindingsConfig(input.cfBindings, errors);
|
|
3362
3405
|
if (bindings !== null) data.cfBindings = bindings;
|
|
3363
3406
|
}
|
|
3407
|
+
if (input.triggers !== void 0) {
|
|
3408
|
+
const triggers = asTriggersConfig(input.triggers, errors);
|
|
3409
|
+
if (triggers !== null) data.triggers = triggers;
|
|
3410
|
+
}
|
|
3364
3411
|
if (errors.length > 0) return { ok: false, errors };
|
|
3365
3412
|
return { ok: true, data };
|
|
3366
3413
|
}
|
|
@@ -3369,13 +3416,30 @@ function renderTsValue(value) {
|
|
|
3369
3416
|
if (typeof value === "string") return `'${value}'`;
|
|
3370
3417
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
3371
3418
|
if (typeof value === "number") return String(value);
|
|
3372
|
-
if (Array.isArray(value))
|
|
3419
|
+
if (Array.isArray(value)) {
|
|
3420
|
+
if (value.every((item) => item === null || ["string", "boolean", "number"].includes(typeof item))) {
|
|
3421
|
+
return `[${value.map(renderTsValue).join(", ")}]`;
|
|
3422
|
+
}
|
|
3423
|
+
return `[${value.map((item) => renderTsValue(item)).join(", ")}]`;
|
|
3424
|
+
}
|
|
3373
3425
|
if (typeof value === "object") {
|
|
3374
|
-
const entries = Object.entries(value).map(([k, v]) => `${k}: ${renderTsValue(v)}`).join(", ");
|
|
3426
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}: ${renderTsValue(v)}`).join(", ");
|
|
3375
3427
|
return `{ ${entries} }`;
|
|
3376
3428
|
}
|
|
3377
3429
|
return String(value);
|
|
3378
3430
|
}
|
|
3431
|
+
function renderTriggersBlock(triggers) {
|
|
3432
|
+
const lines = triggers.map((trigger) => {
|
|
3433
|
+
const parts = [`function: '${trigger.function}'`, `type: '${trigger.type}'`];
|
|
3434
|
+
if (trigger.cron !== void 0) parts.push(`cron: '${trigger.cron}'`);
|
|
3435
|
+
if (trigger.prefix !== void 0) parts.push(`prefix: '${trigger.prefix}'`);
|
|
3436
|
+
if (trigger.enabled !== void 0) parts.push(`enabled: ${trigger.enabled ? "true" : "false"}`);
|
|
3437
|
+
return ` { ${parts.join(", ")} },`;
|
|
3438
|
+
});
|
|
3439
|
+
return ` triggers: [
|
|
3440
|
+
${lines.join("\n")}
|
|
3441
|
+
],`;
|
|
3442
|
+
}
|
|
3379
3443
|
function renderAdepConfig(config2, options = {}) {
|
|
3380
3444
|
const functionsDir = config2.functionsDir ?? DEFAULT_ADEP_CONFIG.functionsDir;
|
|
3381
3445
|
const functionsPrefix = config2.functions_prefix ?? DEFAULT_ADEP_CONFIG.functions_prefix;
|
|
@@ -3399,6 +3463,9 @@ function renderAdepConfig(config2, options = {}) {
|
|
|
3399
3463
|
if (config2.cfBindings !== void 0) {
|
|
3400
3464
|
lines.push(` cfBindings: ${renderTsValue(config2.cfBindings)},`);
|
|
3401
3465
|
}
|
|
3466
|
+
if (config2.triggers !== void 0 && config2.triggers.length > 0) {
|
|
3467
|
+
lines.push(renderTriggersBlock(config2.triggers));
|
|
3468
|
+
}
|
|
3402
3469
|
if (options.extraFields !== void 0) {
|
|
3403
3470
|
for (const [key, value] of Object.entries(options.extraFields)) {
|
|
3404
3471
|
lines.push(` ${key}: ${renderTsValue(value)},`);
|
|
@@ -3676,17 +3743,20 @@ var init_shared_assets = __esm({
|
|
|
3676
3743
|
"@adep/cli": cliVersion,
|
|
3677
3744
|
vue: "^3.5.42",
|
|
3678
3745
|
"vue-router": "^5.0.0",
|
|
3679
|
-
// OIDC 登录(web/src/lib/oidc.ts 运行时 import createAuthClient
|
|
3746
|
+
// OIDC 登录(web/src/lib/oidc.ts 运行时 import createAuthClient)。
|
|
3680
3747
|
"@adep/auth-client": "^0.2.2"
|
|
3681
3748
|
},
|
|
3682
3749
|
devDependencies: {
|
|
3683
3750
|
"@adep/types": "^0.2.0",
|
|
3751
|
+
// 官方路由/响应包(FN-029):函数用 @adep/runtime/route + response(沙箱内置白名单,
|
|
3752
|
+
// 运行期零安装);devDependencies 仅为模板 _shared 单测提供类型与实现解析。
|
|
3753
|
+
"@adep/runtime": "^0.3.0",
|
|
3684
3754
|
"@vitejs/plugin-vue": "^6.0.0",
|
|
3685
3755
|
"esbuild-wasm": "^0.25.12",
|
|
3686
3756
|
typescript: "5.9.3",
|
|
3687
3757
|
vite: "^7.0.0",
|
|
3688
3758
|
// vitest 用 ^3.2.4:vitest 4.x 的 packument 触发 npm 10.9 arborist edgesOut 崩溃(官方源同样),
|
|
3689
|
-
// 与
|
|
3759
|
+
// 与 countdown 模板保持一致。
|
|
3690
3760
|
vitest: "^3.2.4"
|
|
3691
3761
|
},
|
|
3692
3762
|
engines: {
|
|
@@ -4110,6 +4180,17 @@ var init_prompt = __esm({
|
|
|
4110
4180
|
}
|
|
4111
4181
|
});
|
|
4112
4182
|
|
|
4183
|
+
// packages/runtime/src/shared/shared-source.ts
|
|
4184
|
+
var FUNCTION_SHARED_DIR, SHARED_SOURCE_ROOT, FUNCTION_SHARED_MAX_TOTAL_BYTES;
|
|
4185
|
+
var init_shared_source = __esm({
|
|
4186
|
+
"packages/runtime/src/shared/shared-source.ts"() {
|
|
4187
|
+
"use strict";
|
|
4188
|
+
FUNCTION_SHARED_DIR = "_shared";
|
|
4189
|
+
SHARED_SOURCE_ROOT = `functions/${FUNCTION_SHARED_DIR}/`;
|
|
4190
|
+
FUNCTION_SHARED_MAX_TOTAL_BYTES = 256 * 1024;
|
|
4191
|
+
}
|
|
4192
|
+
});
|
|
4193
|
+
|
|
4113
4194
|
// shared/deploy-bundle/manifest.ts
|
|
4114
4195
|
var CURRENT_SCHEMA_VERSION, BundleContentKind, ALLOWED_CONTENT_KINDS;
|
|
4115
4196
|
var init_manifest = __esm({
|
|
@@ -19885,13 +19966,15 @@ function resolveDraftEntry(name, files) {
|
|
|
19885
19966
|
}
|
|
19886
19967
|
return null;
|
|
19887
19968
|
}
|
|
19888
|
-
var MAX_TOTAL_SOURCE_BYTES, ENTRY_FILE, ENTRY_SOURCE_EXTS, FnError;
|
|
19969
|
+
var MAX_TOTAL_SOURCE_BYTES, ENTRY_FILE, ENTRY_SOURCE_EXTS, FUNCTION_SHARED_DIR2, FnError;
|
|
19889
19970
|
var init_domain = __esm({
|
|
19890
19971
|
"packages/runtime/src/functions/domain.ts"() {
|
|
19891
19972
|
"use strict";
|
|
19973
|
+
init_shared_source();
|
|
19892
19974
|
MAX_TOTAL_SOURCE_BYTES = 256 * 1024;
|
|
19893
19975
|
ENTRY_FILE = "index.ts";
|
|
19894
19976
|
ENTRY_SOURCE_EXTS = [".ts", ".js", ".mjs", ".cjs"];
|
|
19977
|
+
FUNCTION_SHARED_DIR2 = `${FUNCTION_SHARED_DIR}/`;
|
|
19895
19978
|
FnError = class extends Error {
|
|
19896
19979
|
status;
|
|
19897
19980
|
code;
|
|
@@ -23091,7 +23174,7 @@ var init_invoke = __esm({
|
|
|
23091
23174
|
});
|
|
23092
23175
|
|
|
23093
23176
|
// shared/builtin-deps.ts
|
|
23094
|
-
var CF_COMPAT_SPECIFIER, STATE_OBJECTS_SPECIFIER, CRYPTO_SPECIFIER, ROUTE_SPECIFIER, BUILTIN_DEPS, BUILTIN_DEPS_CSV;
|
|
23177
|
+
var CF_COMPAT_SPECIFIER, STATE_OBJECTS_SPECIFIER, CRYPTO_SPECIFIER, ROUTE_SPECIFIER, RESPONSE_SPECIFIER, BUILTIN_DEPS, BUILTIN_DEPS_CSV;
|
|
23095
23178
|
var init_builtin_deps = __esm({
|
|
23096
23179
|
"shared/builtin-deps.ts"() {
|
|
23097
23180
|
"use strict";
|
|
@@ -23099,11 +23182,13 @@ var init_builtin_deps = __esm({
|
|
|
23099
23182
|
STATE_OBJECTS_SPECIFIER = "@adep/runtime/state-objects";
|
|
23100
23183
|
CRYPTO_SPECIFIER = "@adep/crypto";
|
|
23101
23184
|
ROUTE_SPECIFIER = "@adep/runtime/route";
|
|
23185
|
+
RESPONSE_SPECIFIER = "@adep/runtime/response";
|
|
23102
23186
|
BUILTIN_DEPS = [
|
|
23103
23187
|
CF_COMPAT_SPECIFIER,
|
|
23104
23188
|
STATE_OBJECTS_SPECIFIER,
|
|
23105
23189
|
CRYPTO_SPECIFIER,
|
|
23106
23190
|
ROUTE_SPECIFIER,
|
|
23191
|
+
RESPONSE_SPECIFIER,
|
|
23107
23192
|
"axios",
|
|
23108
23193
|
"lodash",
|
|
23109
23194
|
"dayjs",
|
|
@@ -23123,7 +23208,8 @@ var init_builtin_deps2 = __esm({
|
|
|
23123
23208
|
CF_COMPAT_SPECIFIER,
|
|
23124
23209
|
STATE_OBJECTS_SPECIFIER,
|
|
23125
23210
|
CRYPTO_SPECIFIER,
|
|
23126
|
-
ROUTE_SPECIFIER
|
|
23211
|
+
ROUTE_SPECIFIER,
|
|
23212
|
+
RESPONSE_SPECIFIER
|
|
23127
23213
|
];
|
|
23128
23214
|
}
|
|
23129
23215
|
});
|
|
@@ -24351,30 +24437,20 @@ var init_worker_entry = __esm({
|
|
|
24351
24437
|
function mergeSharedSource(files, shared) {
|
|
24352
24438
|
const merged = { ...files };
|
|
24353
24439
|
for (const file2 of shared) {
|
|
24354
|
-
if (!file2.path.startsWith(
|
|
24355
|
-
const rel = file2.path.slice(
|
|
24440
|
+
if (!file2.path.startsWith(SHARED_SOURCE_ROOT2)) continue;
|
|
24441
|
+
const rel = file2.path.slice(SHARED_SOURCE_ROOT2.length);
|
|
24356
24442
|
if (rel === "") continue;
|
|
24357
|
-
merged[`${
|
|
24443
|
+
merged[`${FUNCTION_SHARED_DIR3}${rel}`] = file2.content;
|
|
24358
24444
|
}
|
|
24359
24445
|
return merged;
|
|
24360
24446
|
}
|
|
24361
|
-
var
|
|
24447
|
+
var SHARED_SOURCE_ROOT2, FUNCTION_SHARED_DIR3;
|
|
24362
24448
|
var init_function_source = __esm({
|
|
24363
24449
|
"packages/runtime/src/shared/function-source.ts"() {
|
|
24364
24450
|
"use strict";
|
|
24365
|
-
|
|
24366
|
-
|
|
24367
|
-
|
|
24368
|
-
});
|
|
24369
|
-
|
|
24370
|
-
// shared/shared-source.ts
|
|
24371
|
-
var FUNCTION_SHARED_DIR2, SHARED_SOURCE_ROOT2, FUNCTION_SHARED_MAX_TOTAL_BYTES;
|
|
24372
|
-
var init_shared_source = __esm({
|
|
24373
|
-
"shared/shared-source.ts"() {
|
|
24374
|
-
"use strict";
|
|
24375
|
-
FUNCTION_SHARED_DIR2 = "_shared";
|
|
24376
|
-
SHARED_SOURCE_ROOT2 = `functions/${FUNCTION_SHARED_DIR2}/`;
|
|
24377
|
-
FUNCTION_SHARED_MAX_TOTAL_BYTES = 256 * 1024;
|
|
24451
|
+
init_shared_source();
|
|
24452
|
+
SHARED_SOURCE_ROOT2 = SHARED_SOURCE_ROOT;
|
|
24453
|
+
FUNCTION_SHARED_DIR3 = `${FUNCTION_SHARED_DIR}/`;
|
|
24378
24454
|
}
|
|
24379
24455
|
});
|
|
24380
24456
|
|
|
@@ -24435,9 +24511,9 @@ async function collectFunctions(dir) {
|
|
|
24435
24511
|
function mergeSharedIntoFunctions(files) {
|
|
24436
24512
|
const shared = [];
|
|
24437
24513
|
for (const [path, content] of Object.entries(files)) {
|
|
24438
|
-
if (!path.startsWith(`${
|
|
24514
|
+
if (!path.startsWith(`${FUNCTION_SHARED_DIR}/`)) continue;
|
|
24439
24515
|
shared.push({
|
|
24440
|
-
path: `${
|
|
24516
|
+
path: `${SHARED_SOURCE_ROOT}${path.slice(FUNCTION_SHARED_DIR.length + 1)}`,
|
|
24441
24517
|
content
|
|
24442
24518
|
});
|
|
24443
24519
|
}
|
|
@@ -25171,7 +25247,7 @@ function collectFunctionNames(manifest) {
|
|
|
25171
25247
|
if (rest.length === 0) continue;
|
|
25172
25248
|
const slash = rest.indexOf("/");
|
|
25173
25249
|
const name = slash === -1 ? rest.replace(/\.[^./]+$/, "") : rest.slice(0, slash);
|
|
25174
|
-
if (name.length === 0 || name ===
|
|
25250
|
+
if (name.length === 0 || name === FUNCTION_SHARED_DIR) continue;
|
|
25175
25251
|
names.add(name);
|
|
25176
25252
|
}
|
|
25177
25253
|
return [...names].toSorted((left, right) => left.localeCompare(right));
|
|
@@ -25626,6 +25702,7 @@ var SITE_PREFIX, SHELL_SITE_DIR, SHELL_OUT_DIR, REPORT_FILE, TOP_ASSETS_LIMIT, D
|
|
|
25626
25702
|
var init_app_build = __esm({
|
|
25627
25703
|
"packages/cli/src/app-build.ts"() {
|
|
25628
25704
|
"use strict";
|
|
25705
|
+
init_shared_source();
|
|
25629
25706
|
init_validate();
|
|
25630
25707
|
init_zip();
|
|
25631
25708
|
init_app_config();
|
|
@@ -26165,7 +26242,7 @@ async function collectFunctionFiles(functionsDir) {
|
|
|
26165
26242
|
(entry) => entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts")
|
|
26166
26243
|
).map((entry) => basename4(entry.name, ".ts")).toSorted();
|
|
26167
26244
|
const dirNames = dirents.filter(
|
|
26168
|
-
(entry) => entry.isDirectory() && entry.name !==
|
|
26245
|
+
(entry) => entry.isDirectory() && entry.name !== FUNCTION_SHARED_DIR && !IGNORED_DIRS.has(entry.name)
|
|
26169
26246
|
).map((entry) => entry.name).toSorted();
|
|
26170
26247
|
const conflicting = flatNames.filter((name) => dirNames.includes(name));
|
|
26171
26248
|
if (conflicting.length > 0) throw new AmbiguousFunctionError(functionsDir, conflicting);
|
|
@@ -26207,7 +26284,7 @@ function sha256Hex(content) {
|
|
|
26207
26284
|
return createHash4("sha256").update(content).digest("hex");
|
|
26208
26285
|
}
|
|
26209
26286
|
async function collectShared(functionsDir) {
|
|
26210
|
-
const sharedDir = join20(functionsDir,
|
|
26287
|
+
const sharedDir = join20(functionsDir, FUNCTION_SHARED_DIR);
|
|
26211
26288
|
const files = {};
|
|
26212
26289
|
const walk = async (dir) => {
|
|
26213
26290
|
let entries;
|
|
@@ -26225,7 +26302,7 @@ async function collectShared(functionsDir) {
|
|
|
26225
26302
|
if (!isRuntimeSource2(entry.name)) continue;
|
|
26226
26303
|
const full = join20(dir, entry.name);
|
|
26227
26304
|
const rel = relative5(sharedDir, full).split(sep).join("/");
|
|
26228
|
-
files[`${
|
|
26305
|
+
files[`${SHARED_SOURCE_ROOT}${rel}`] = await readFile16(full, "utf8");
|
|
26229
26306
|
}
|
|
26230
26307
|
};
|
|
26231
26308
|
await walk(sharedDir);
|
|
@@ -26334,7 +26411,7 @@ async function deploy(paths, options) {
|
|
|
26334
26411
|
functions: deployed
|
|
26335
26412
|
};
|
|
26336
26413
|
}
|
|
26337
|
-
var
|
|
26414
|
+
var AmbiguousFunctionError, IGNORED_DIRS;
|
|
26338
26415
|
var init_deploy = __esm({
|
|
26339
26416
|
"packages/cli/src/deploy.ts"() {
|
|
26340
26417
|
"use strict";
|
|
@@ -26343,7 +26420,8 @@ var init_deploy = __esm({
|
|
|
26343
26420
|
init_client();
|
|
26344
26421
|
init_dev();
|
|
26345
26422
|
init_net();
|
|
26346
|
-
|
|
26423
|
+
init_shared_source();
|
|
26424
|
+
init_shared_source();
|
|
26347
26425
|
AmbiguousFunctionError = class extends CliError {
|
|
26348
26426
|
constructor(functionsDir, names) {
|
|
26349
26427
|
super(
|
|
@@ -26355,7 +26433,6 @@ var init_deploy = __esm({
|
|
|
26355
26433
|
}
|
|
26356
26434
|
};
|
|
26357
26435
|
IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
26358
|
-
SHARED_DIR_NAME = "_shared";
|
|
26359
26436
|
}
|
|
26360
26437
|
});
|
|
26361
26438
|
|
package/dist/vite.js
CHANGED
|
@@ -92,10 +92,16 @@ import { resolve } from "node:path";
|
|
|
92
92
|
// packages/runtime/src/functions/deps/manifest.ts
|
|
93
93
|
import { createHash } from "node:crypto";
|
|
94
94
|
|
|
95
|
+
// packages/runtime/src/shared/shared-source.ts
|
|
96
|
+
var FUNCTION_SHARED_DIR = "_shared";
|
|
97
|
+
var SHARED_SOURCE_ROOT = `functions/${FUNCTION_SHARED_DIR}/`;
|
|
98
|
+
var FUNCTION_SHARED_MAX_TOTAL_BYTES = 256 * 1024;
|
|
99
|
+
|
|
95
100
|
// packages/runtime/src/functions/domain.ts
|
|
96
101
|
var MAX_TOTAL_SOURCE_BYTES = 256 * 1024;
|
|
97
102
|
var ENTRY_FILE = "index.ts";
|
|
98
103
|
var ENTRY_SOURCE_EXTS = [".ts", ".js", ".mjs", ".cjs"];
|
|
104
|
+
var FUNCTION_SHARED_DIR2 = `${FUNCTION_SHARED_DIR}/`;
|
|
99
105
|
function flatEntryCandidates(name) {
|
|
100
106
|
return ENTRY_SOURCE_EXTS.map((ext) => `${name}${ext}`);
|
|
101
107
|
}
|
|
@@ -2975,11 +2981,13 @@ var CF_COMPAT_SPECIFIER = "@adep/cf-compat";
|
|
|
2975
2981
|
var STATE_OBJECTS_SPECIFIER = "@adep/runtime/state-objects";
|
|
2976
2982
|
var CRYPTO_SPECIFIER = "@adep/crypto";
|
|
2977
2983
|
var ROUTE_SPECIFIER = "@adep/runtime/route";
|
|
2984
|
+
var RESPONSE_SPECIFIER = "@adep/runtime/response";
|
|
2978
2985
|
var BUILTIN_DEPS = [
|
|
2979
2986
|
CF_COMPAT_SPECIFIER,
|
|
2980
2987
|
STATE_OBJECTS_SPECIFIER,
|
|
2981
2988
|
CRYPTO_SPECIFIER,
|
|
2982
2989
|
ROUTE_SPECIFIER,
|
|
2990
|
+
RESPONSE_SPECIFIER,
|
|
2983
2991
|
"axios",
|
|
2984
2992
|
"lodash",
|
|
2985
2993
|
"dayjs",
|
|
@@ -2992,7 +3000,8 @@ var LOCAL_BUILTIN_DEPS = [
|
|
|
2992
3000
|
CF_COMPAT_SPECIFIER,
|
|
2993
3001
|
STATE_OBJECTS_SPECIFIER,
|
|
2994
3002
|
CRYPTO_SPECIFIER,
|
|
2995
|
-
ROUTE_SPECIFIER
|
|
3003
|
+
ROUTE_SPECIFIER,
|
|
3004
|
+
RESPONSE_SPECIFIER
|
|
2996
3005
|
];
|
|
2997
3006
|
|
|
2998
3007
|
// packages/cli/src/sim/boundary.ts
|
|
@@ -3095,6 +3104,57 @@ function asCfBindingsConfig(value, errors) {
|
|
|
3095
3104
|
if (r2 !== null && r2 !== void 0) bindings.r2 = r2;
|
|
3096
3105
|
return bindings;
|
|
3097
3106
|
}
|
|
3107
|
+
function asTriggersConfig(value, errors) {
|
|
3108
|
+
if (!Array.isArray(value)) {
|
|
3109
|
+
errors.push(`triggers \u5FC5\u987B\u662F\u6570\u7EC4\uFF0C\u6536\u5230 ${typeNameOf(value)}`);
|
|
3110
|
+
return null;
|
|
3111
|
+
}
|
|
3112
|
+
const out = [];
|
|
3113
|
+
value.forEach((item, i) => {
|
|
3114
|
+
const idx = `triggers[${i}]`;
|
|
3115
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
3116
|
+
errors.push(`${idx} \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u6536\u5230 ${typeNameOf(item)}`);
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
const rec = item;
|
|
3120
|
+
if (typeof rec.function !== "string" || rec.function.length === 0) {
|
|
3121
|
+
errors.push(`${idx}.function \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF0C\u6536\u5230 ${typeNameOf(rec.function)}`);
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
if (rec.type !== "cron" && rec.type !== "storage.upload") {
|
|
3125
|
+
errors.push(`${idx}.type \u5FC5\u987B\u662F 'cron' \u6216 'storage.upload'\uFF0C\u6536\u5230 ${typeNameOf(rec.type)}`);
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
const entry = { function: rec.function, type: rec.type };
|
|
3129
|
+
if (rec.cron !== void 0) {
|
|
3130
|
+
if (typeof rec.cron !== "string") {
|
|
3131
|
+
errors.push(`${idx}.cron \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u6536\u5230 ${typeNameOf(rec.cron)}`);
|
|
3132
|
+
} else if (rec.type === "cron" && rec.cron.trim().length === 0) {
|
|
3133
|
+
errors.push(`${idx}.cron \u4E0D\u80FD\u4E3A\u7A7A\uFF08type='cron' \u5FC5\u987B\u7ED9\u51FA 5 \u5B57\u6BB5 cron \u8868\u8FBE\u5F0F\uFF09`);
|
|
3134
|
+
} else {
|
|
3135
|
+
entry.cron = rec.cron;
|
|
3136
|
+
}
|
|
3137
|
+
} else if (rec.type === "cron") {
|
|
3138
|
+
errors.push(`${idx}.cron \u7F3A\u5931\uFF1Atype='cron' \u5FC5\u987B\u7ED9\u51FA 5 \u5B57\u6BB5 cron \u8868\u8FBE\u5F0F`);
|
|
3139
|
+
}
|
|
3140
|
+
if (rec.prefix !== void 0) {
|
|
3141
|
+
if (typeof rec.prefix !== "string") {
|
|
3142
|
+
errors.push(`${idx}.prefix \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u6536\u5230 ${typeNameOf(rec.prefix)}`);
|
|
3143
|
+
} else {
|
|
3144
|
+
entry.prefix = rec.prefix;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
if (rec.enabled !== void 0) {
|
|
3148
|
+
if (typeof rec.enabled !== "boolean") {
|
|
3149
|
+
errors.push(`${idx}.enabled \u5FC5\u987B\u662F\u5E03\u5C14\u503C\uFF0C\u6536\u5230 ${typeNameOf(rec.enabled)}`);
|
|
3150
|
+
} else {
|
|
3151
|
+
entry.enabled = rec.enabled;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
out.push(entry);
|
|
3155
|
+
});
|
|
3156
|
+
return out;
|
|
3157
|
+
}
|
|
3098
3158
|
function parseAdepConfig(raw) {
|
|
3099
3159
|
if (raw === null || raw === void 0) {
|
|
3100
3160
|
return { ok: true, data: { ...DEFAULT_ADEP_CONFIG } };
|
|
@@ -3159,6 +3219,10 @@ function parseAdepConfig(raw) {
|
|
|
3159
3219
|
const bindings = asCfBindingsConfig(input.cfBindings, errors);
|
|
3160
3220
|
if (bindings !== null) data.cfBindings = bindings;
|
|
3161
3221
|
}
|
|
3222
|
+
if (input.triggers !== void 0) {
|
|
3223
|
+
const triggers = asTriggersConfig(input.triggers, errors);
|
|
3224
|
+
if (triggers !== null) data.triggers = triggers;
|
|
3225
|
+
}
|
|
3162
3226
|
if (errors.length > 0) return { ok: false, errors };
|
|
3163
3227
|
return { ok: true, data };
|
|
3164
3228
|
}
|
|
@@ -4240,24 +4304,19 @@ import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
|
|
|
4240
4304
|
import { join as join9 } from "node:path";
|
|
4241
4305
|
|
|
4242
4306
|
// packages/runtime/src/shared/function-source.ts
|
|
4243
|
-
var
|
|
4244
|
-
var
|
|
4307
|
+
var SHARED_SOURCE_ROOT2 = SHARED_SOURCE_ROOT;
|
|
4308
|
+
var FUNCTION_SHARED_DIR3 = `${FUNCTION_SHARED_DIR}/`;
|
|
4245
4309
|
function mergeSharedSource(files, shared) {
|
|
4246
4310
|
const merged = { ...files };
|
|
4247
4311
|
for (const file of shared) {
|
|
4248
|
-
if (!file.path.startsWith(
|
|
4249
|
-
const rel = file.path.slice(
|
|
4312
|
+
if (!file.path.startsWith(SHARED_SOURCE_ROOT2)) continue;
|
|
4313
|
+
const rel = file.path.slice(SHARED_SOURCE_ROOT2.length);
|
|
4250
4314
|
if (rel === "") continue;
|
|
4251
|
-
merged[`${
|
|
4315
|
+
merged[`${FUNCTION_SHARED_DIR3}${rel}`] = file.content;
|
|
4252
4316
|
}
|
|
4253
4317
|
return merged;
|
|
4254
4318
|
}
|
|
4255
4319
|
|
|
4256
|
-
// shared/shared-source.ts
|
|
4257
|
-
var FUNCTION_SHARED_DIR2 = "_shared";
|
|
4258
|
-
var SHARED_SOURCE_ROOT2 = `functions/${FUNCTION_SHARED_DIR2}/`;
|
|
4259
|
-
var FUNCTION_SHARED_MAX_TOTAL_BYTES = 256 * 1024;
|
|
4260
|
-
|
|
4261
4320
|
// packages/cli/src/dev-shared.ts
|
|
4262
4321
|
var BEARER_TOKEN_RE = /^Bearer\s+(.+)$/i;
|
|
4263
4322
|
function bearerUserResolverFromEnv() {
|
|
@@ -4314,9 +4373,9 @@ async function collectFunctions(dir) {
|
|
|
4314
4373
|
function mergeSharedIntoFunctions(files) {
|
|
4315
4374
|
const shared = [];
|
|
4316
4375
|
for (const [path, content] of Object.entries(files)) {
|
|
4317
|
-
if (!path.startsWith(`${
|
|
4376
|
+
if (!path.startsWith(`${FUNCTION_SHARED_DIR}/`)) continue;
|
|
4318
4377
|
shared.push({
|
|
4319
|
-
path: `${
|
|
4378
|
+
path: `${SHARED_SOURCE_ROOT}${path.slice(FUNCTION_SHARED_DIR.length + 1)}`,
|
|
4320
4379
|
content
|
|
4321
4380
|
});
|
|
4322
4381
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adep/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "`adep` 命令行工具包位:登录 / 初始化 / 本地调试 / 部署 / 数据库维护 / 云存储 / 静态托管 / 微前端组件(widget),实现见任务单 CLI-001 ~ CLI-003 与 FE-002;子路径 `@adep/cli/vite` 提供云函数 vite 插件(CLI-014)",
|
|
6
6
|
"bin": {
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"commander": "14.0.2",
|
|
23
23
|
"esbuild": "0.25.12",
|
|
24
24
|
"zod": "^4.4.3",
|
|
25
|
-
"@adep/
|
|
26
|
-
"@adep/
|
|
25
|
+
"@adep/cf-compat": "0.1.1",
|
|
26
|
+
"@adep/runtime": "0.3.2",
|
|
27
27
|
"@adep/crypto": "0.1.0",
|
|
28
|
-
"@adep/
|
|
28
|
+
"@adep/vite-plugin": "0.1.8"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@adep/types": "0.3.0"
|