@neurocode-ai/enterprise 1.18.8
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/README.md +32 -0
- package/package.json +45 -0
- package/public/apple-touch-icon-v3.png +1 -0
- package/public/apple-touch-icon.png +1 -0
- package/public/favicon-96x96-v3.png +1 -0
- package/public/favicon-96x96.png +1 -0
- package/public/favicon-v3.ico +1 -0
- package/public/favicon-v3.svg +1 -0
- package/public/favicon.ico +1 -0
- package/public/favicon.svg +1 -0
- package/public/site.webmanifest +1 -0
- package/public/social-share-zen.png +1 -0
- package/public/social-share.png +1 -0
- package/public/web-app-manifest-192x192.png +1 -0
- package/public/web-app-manifest-512x512.png +1 -0
- package/script/scrap.ts +15 -0
- package/src/app.css +2 -0
- package/src/app.tsx +94 -0
- package/src/core/share.ts +232 -0
- package/src/core/storage.ts +129 -0
- package/src/custom-elements.d.ts +1 -0
- package/src/entry-client.tsx +4 -0
- package/src/entry-server.tsx +38 -0
- package/src/global.d.ts +5 -0
- package/src/routes/[...404].tsx +25 -0
- package/src/routes/api/[...path].ts +171 -0
- package/src/routes/index.tsx +3 -0
- package/src/routes/share/[shareID].tsx +417 -0
- package/src/routes/share.tsx +5 -0
- package/sst-env.d.ts +10 -0
- package/test/core/share.test.ts +292 -0
- package/test/core/storage.test.ts +64 -0
- package/test-debug.ts +40 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SolidStart
|
|
2
|
+
|
|
3
|
+
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
|
|
4
|
+
|
|
5
|
+
## Creating a project
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# create a new project in the current directory
|
|
9
|
+
npm init solid@latest
|
|
10
|
+
|
|
11
|
+
# create a new project in my-app
|
|
12
|
+
npm init solid@latest my-app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Developing
|
|
16
|
+
|
|
17
|
+
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run dev
|
|
21
|
+
|
|
22
|
+
# or start the server and open the app in a new browser tab
|
|
23
|
+
npm run dev -- --open
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Building
|
|
27
|
+
|
|
28
|
+
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
|
|
29
|
+
|
|
30
|
+
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
|
|
31
|
+
|
|
32
|
+
## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neurocode-ai/enterprise",
|
|
3
|
+
"version": "1.18.8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"typecheck": "tsgo --noEmit",
|
|
8
|
+
"dev": "vite dev",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"build:cloudflare": "OPENCODE_DEPLOYMENT_TARGET=cloudflare vite build",
|
|
11
|
+
"start": "vite start",
|
|
12
|
+
"shell-prod": "sst shell --target Teams --stage production"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@neurocode-ai/core": "1.18.8",
|
|
16
|
+
"@neurocode-ai/session-ui": "1.18.8",
|
|
17
|
+
"@neurocode-ai/ui": "1.18.8",
|
|
18
|
+
"aws4fetch": "^1.0.20",
|
|
19
|
+
"@pierre/diffs": "1.2.10",
|
|
20
|
+
"@solidjs/router": "0.15.4",
|
|
21
|
+
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
|
22
|
+
"@solidjs/meta": "0.29.4",
|
|
23
|
+
"@hono/standard-validator": "0.2.0",
|
|
24
|
+
"hono": "4.10.7",
|
|
25
|
+
"hono-openapi": "1.1.2",
|
|
26
|
+
"js-base64": "3.7.7",
|
|
27
|
+
"luxon": "3.6.1",
|
|
28
|
+
"nitro": "3.0.1-alpha.1",
|
|
29
|
+
"solid-js": "1.9.10",
|
|
30
|
+
"zod": "4.1.8"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@cloudflare/workers-types": "4.20251008.0",
|
|
34
|
+
"@tailwindcss/vite": "4.1.11",
|
|
35
|
+
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
|
36
|
+
"@types/bun": "1.3.13",
|
|
37
|
+
"@types/luxon": "3.7.1",
|
|
38
|
+
"tailwindcss": "4.1.11",
|
|
39
|
+
"typescript": "5.8.2",
|
|
40
|
+
"vite": "7.1.4"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/favicon/apple-touch-icon-v3.png
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/favicon/apple-touch-icon.png
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/favicon/favicon-96x96-v3.png
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/favicon/favicon-96x96.png
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/favicon/favicon-v3.ico
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/favicon/favicon-v3.svg
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/favicon/favicon.ico
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/favicon/favicon.svg
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/favicon/site.webmanifest
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/images/social-share-zen.png
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/images/social-share.png
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/favicon/web-app-manifest-192x192.png
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/assets/favicon/web-app-manifest-512x512.png
|
package/script/scrap.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Storage } from "../src/core/storage"
|
|
2
|
+
|
|
3
|
+
// read share id from args
|
|
4
|
+
const args = process.argv.slice(2)
|
|
5
|
+
if (args.length !== 1) {
|
|
6
|
+
console.error("Usage: bun script/scrap.ts <shareID>")
|
|
7
|
+
process.exit(1)
|
|
8
|
+
}
|
|
9
|
+
const shareID = args[0]
|
|
10
|
+
|
|
11
|
+
await Storage.remove(["share", shareID])
|
|
12
|
+
const list = await Storage.list({ prefix: ["share_data", shareID] })
|
|
13
|
+
for (const item of list) {
|
|
14
|
+
await Storage.remove(item)
|
|
15
|
+
}
|
package/src/app.css
ADDED
package/src/app.tsx
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Router } from "@solidjs/router"
|
|
2
|
+
import { FileRoutes } from "@solidjs/start/router"
|
|
3
|
+
import { Font } from "@neurocode-ai/ui/font"
|
|
4
|
+
import { MetaProvider } from "@solidjs/meta"
|
|
5
|
+
import { MarkedProvider } from "@neurocode-ai/ui/context/marked"
|
|
6
|
+
import { DialogProvider } from "@neurocode-ai/ui/context/dialog"
|
|
7
|
+
import { I18nProvider, type UiI18nParams } from "@neurocode-ai/ui/context"
|
|
8
|
+
import { dict as uiEn } from "@neurocode-ai/ui/i18n/en"
|
|
9
|
+
import { dict as uiZh } from "@neurocode-ai/ui/i18n/zh"
|
|
10
|
+
import { createEffect, createMemo, Suspense, type ParentProps } from "solid-js"
|
|
11
|
+
import { getRequestEvent } from "solid-js/web"
|
|
12
|
+
import "./app.css"
|
|
13
|
+
import { Favicon } from "@neurocode-ai/ui/favicon"
|
|
14
|
+
|
|
15
|
+
function resolveTemplate(text: string, params?: UiI18nParams) {
|
|
16
|
+
if (!params) return text
|
|
17
|
+
return text.replace(/{{\s*([^}]+?)\s*}}/g, (_, rawKey) => {
|
|
18
|
+
const key = String(rawKey)
|
|
19
|
+
const value = params[key]
|
|
20
|
+
return value === undefined ? "" : String(value)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function detectLocaleFromHeader(header: string | null | undefined) {
|
|
25
|
+
if (!header) return
|
|
26
|
+
for (const item of header.split(",")) {
|
|
27
|
+
const value = item.trim().split(";")[0]?.toLowerCase()
|
|
28
|
+
if (!value) continue
|
|
29
|
+
if (value.startsWith("zh")) return "zh" as const
|
|
30
|
+
if (value.startsWith("en")) return "en" as const
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function detectLocale() {
|
|
35
|
+
const event = getRequestEvent()
|
|
36
|
+
const header = event?.request.headers.get("accept-language")
|
|
37
|
+
const headerLocale = detectLocaleFromHeader(header)
|
|
38
|
+
if (headerLocale) return headerLocale
|
|
39
|
+
|
|
40
|
+
if (typeof document === "object") {
|
|
41
|
+
const value = document.documentElement.lang?.toLowerCase() ?? ""
|
|
42
|
+
if (value.startsWith("zh")) return "zh" as const
|
|
43
|
+
if (value.startsWith("en")) return "en" as const
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof navigator === "object") {
|
|
47
|
+
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
|
|
48
|
+
for (const language of languages) {
|
|
49
|
+
if (!language) continue
|
|
50
|
+
if (language.toLowerCase().startsWith("zh")) return "zh" as const
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return "en" as const
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function UiI18nBridge(props: ParentProps) {
|
|
58
|
+
const locale = createMemo(() => detectLocale())
|
|
59
|
+
const zh = uiZh as Partial<Record<string, string>>
|
|
60
|
+
const t = (key: keyof typeof uiEn, params?: UiI18nParams) => {
|
|
61
|
+
const value = locale() === "zh" ? (zh[key] ?? uiEn[key]) : uiEn[key]
|
|
62
|
+
const text = value ?? String(key)
|
|
63
|
+
return resolveTemplate(text, params)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
createEffect(() => {
|
|
67
|
+
if (typeof document !== "object") return
|
|
68
|
+
document.documentElement.lang = locale()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return <I18nProvider value={{ locale, t }}>{props.children}</I18nProvider>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default function App() {
|
|
75
|
+
return (
|
|
76
|
+
<Router
|
|
77
|
+
root={(props) => (
|
|
78
|
+
<MetaProvider>
|
|
79
|
+
<DialogProvider>
|
|
80
|
+
<MarkedProvider>
|
|
81
|
+
<Favicon />
|
|
82
|
+
<Font />
|
|
83
|
+
<UiI18nBridge>
|
|
84
|
+
<Suspense>{props.children}</Suspense>
|
|
85
|
+
</UiI18nBridge>
|
|
86
|
+
</MarkedProvider>
|
|
87
|
+
</DialogProvider>
|
|
88
|
+
</MetaProvider>
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
<FileRoutes />
|
|
92
|
+
</Router>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Message, Model, Part, Session, SnapshotFileDiff } from "@neurocode-ai/sdk/v2"
|
|
2
|
+
import { iife } from "@neurocode-ai/core/util/iife"
|
|
3
|
+
import z from "zod"
|
|
4
|
+
import { Storage } from "./storage"
|
|
5
|
+
|
|
6
|
+
function fn<T extends z.ZodType, Result>(schema: T, cb: (input: z.infer<T>) => Result) {
|
|
7
|
+
return (input: z.infer<T>) => cb(schema.parse(input))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export namespace Share {
|
|
11
|
+
export const Info = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
secret: z.string(),
|
|
14
|
+
sessionID: z.string(),
|
|
15
|
+
})
|
|
16
|
+
export type Info = z.infer<typeof Info>
|
|
17
|
+
|
|
18
|
+
export const Data = z.discriminatedUnion("type", [
|
|
19
|
+
z.object({
|
|
20
|
+
type: z.literal("session"),
|
|
21
|
+
data: z.custom<Session>(),
|
|
22
|
+
}),
|
|
23
|
+
z.object({
|
|
24
|
+
type: z.literal("message"),
|
|
25
|
+
data: z.custom<Message>(),
|
|
26
|
+
}),
|
|
27
|
+
z.object({
|
|
28
|
+
type: z.literal("part"),
|
|
29
|
+
data: z.custom<Part>(),
|
|
30
|
+
}),
|
|
31
|
+
z.object({
|
|
32
|
+
type: z.literal("session_diff"),
|
|
33
|
+
data: z.custom<SnapshotFileDiff[]>(),
|
|
34
|
+
}),
|
|
35
|
+
z.object({
|
|
36
|
+
type: z.literal("model"),
|
|
37
|
+
data: z.custom<Model[]>(),
|
|
38
|
+
}),
|
|
39
|
+
])
|
|
40
|
+
export type Data = z.infer<typeof Data>
|
|
41
|
+
|
|
42
|
+
type Snapshot = {
|
|
43
|
+
data: Data[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Compaction = {
|
|
47
|
+
event?: string
|
|
48
|
+
data: Data[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function key(item: Data) {
|
|
52
|
+
switch (item.type) {
|
|
53
|
+
case "session":
|
|
54
|
+
return "session"
|
|
55
|
+
case "message":
|
|
56
|
+
return `message/${item.data.id}`
|
|
57
|
+
case "part":
|
|
58
|
+
return `part/${item.data.messageID}/${item.data.id}`
|
|
59
|
+
case "session_diff":
|
|
60
|
+
return "session_diff"
|
|
61
|
+
case "model":
|
|
62
|
+
return "model"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function merge(...items: Data[][]) {
|
|
67
|
+
const map = new Map<string, Data>()
|
|
68
|
+
for (const list of items) {
|
|
69
|
+
for (const item of list) {
|
|
70
|
+
map.set(key(item), item)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return Array.from(map.entries())
|
|
74
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
75
|
+
.map(([, item]) => item)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function readSnapshot(shareID: string) {
|
|
79
|
+
return (await Storage.read<Snapshot>(["share_snapshot", shareID]))?.data
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function writeSnapshot(shareID: string, data: Data[]) {
|
|
83
|
+
await Storage.write<Snapshot>(["share_snapshot", shareID], { data })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function legacy(shareID: string) {
|
|
87
|
+
const compaction: Compaction = (await Storage.read<Compaction>(["share_compaction", shareID])) ?? {
|
|
88
|
+
data: [],
|
|
89
|
+
event: undefined,
|
|
90
|
+
}
|
|
91
|
+
const list = await Storage.list({
|
|
92
|
+
prefix: ["share_event", shareID],
|
|
93
|
+
before: compaction.event,
|
|
94
|
+
}).then((x) => x.toReversed())
|
|
95
|
+
if (list.length === 0) {
|
|
96
|
+
if (compaction.data.length > 0) await writeSnapshot(shareID, compaction.data)
|
|
97
|
+
return compaction.data
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const next = merge(
|
|
101
|
+
compaction.data,
|
|
102
|
+
await Promise.all(list.map(async (event) => await Storage.read<Data[]>(event))).then((x) =>
|
|
103
|
+
x.flatMap((item) => item ?? []),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
await Promise.all([
|
|
108
|
+
Storage.write(["share_compaction", shareID], {
|
|
109
|
+
event: list.at(-1)?.at(-1),
|
|
110
|
+
data: next,
|
|
111
|
+
}),
|
|
112
|
+
writeSnapshot(shareID, next),
|
|
113
|
+
])
|
|
114
|
+
return next
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const create = fn(z.object({ sessionID: z.string() }), async (body) => {
|
|
118
|
+
const isTest = process.env.NODE_ENV === "test" || body.sessionID.startsWith("test_")
|
|
119
|
+
const info: Info = {
|
|
120
|
+
id: (isTest ? "test_" : "") + body.sessionID.slice(-8),
|
|
121
|
+
sessionID: body.sessionID,
|
|
122
|
+
secret: crypto.randomUUID(),
|
|
123
|
+
}
|
|
124
|
+
const exists = await get(info.id)
|
|
125
|
+
if (exists) throw new Errors.AlreadyExists(info.id)
|
|
126
|
+
await Promise.all([Storage.write(["share", info.id], info), writeSnapshot(info.id, [])])
|
|
127
|
+
return info
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
export async function get(id: string) {
|
|
131
|
+
return Storage.read<Info>(["share", id])
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const remove = fn(Info.pick({ id: true, secret: true }), async (body) => {
|
|
135
|
+
const share = await get(body.id)
|
|
136
|
+
if (!share) throw new Errors.NotFound(body.id)
|
|
137
|
+
if (share.secret !== body.secret) throw new Errors.InvalidSecret(body.id)
|
|
138
|
+
await Storage.remove(["share", body.id])
|
|
139
|
+
const groups = await Promise.all([
|
|
140
|
+
Storage.list({ prefix: ["share_snapshot", body.id] }),
|
|
141
|
+
Storage.list({ prefix: ["share_compaction", body.id] }),
|
|
142
|
+
Storage.list({ prefix: ["share_event", body.id] }),
|
|
143
|
+
Storage.list({ prefix: ["share_data", body.id] }),
|
|
144
|
+
])
|
|
145
|
+
for (const item of groups.flat()) {
|
|
146
|
+
await Storage.remove(item)
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
export const removeAdmin = fn(Info.pick({ id: true }), async (body) => {
|
|
151
|
+
const share = await get(body.id)
|
|
152
|
+
if (!share) throw new Errors.NotFound(body.id)
|
|
153
|
+
await remove({ id: share.id, secret: share.secret })
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
export const sync = fn(
|
|
157
|
+
z.object({
|
|
158
|
+
share: Info.pick({ id: true, secret: true }),
|
|
159
|
+
data: Data.array(),
|
|
160
|
+
}),
|
|
161
|
+
async (input) => {
|
|
162
|
+
const share = await get(input.share.id)
|
|
163
|
+
if (!share) throw new Errors.NotFound(input.share.id)
|
|
164
|
+
if (share.secret !== input.share.secret) throw new Errors.InvalidSecret(input.share.id)
|
|
165
|
+
const data = (await readSnapshot(input.share.id)) ?? (await legacy(input.share.id))
|
|
166
|
+
await writeSnapshot(input.share.id, merge(data, input.data))
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
export async function data(shareID: string) {
|
|
171
|
+
return (await readSnapshot(shareID)) ?? legacy(shareID)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const syncOld = fn(
|
|
175
|
+
z.object({
|
|
176
|
+
share: Info.pick({ id: true, secret: true }),
|
|
177
|
+
data: Data.array(),
|
|
178
|
+
}),
|
|
179
|
+
async (input) => {
|
|
180
|
+
const share = await get(input.share.id)
|
|
181
|
+
if (!share) throw new Errors.NotFound(input.share.id)
|
|
182
|
+
if (share.secret !== input.share.secret) throw new Errors.InvalidSecret(input.share.id)
|
|
183
|
+
const promises = []
|
|
184
|
+
for (const item of input.data) {
|
|
185
|
+
promises.push(
|
|
186
|
+
iife(async () => {
|
|
187
|
+
switch (item.type) {
|
|
188
|
+
case "session":
|
|
189
|
+
await Storage.write(["share_data", input.share.id, "session"], item.data)
|
|
190
|
+
break
|
|
191
|
+
case "message": {
|
|
192
|
+
const data = item.data as Message
|
|
193
|
+
await Storage.write(["share_data", input.share.id, "message", data.id], item.data)
|
|
194
|
+
break
|
|
195
|
+
}
|
|
196
|
+
case "part": {
|
|
197
|
+
const data = item.data as Part
|
|
198
|
+
await Storage.write(["share_data", input.share.id, "part", data.messageID, data.id], item.data)
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
case "session_diff":
|
|
202
|
+
await Storage.write(["share_data", input.share.id, "session_diff"], item.data)
|
|
203
|
+
break
|
|
204
|
+
case "model":
|
|
205
|
+
await Storage.write(["share_data", input.share.id, "model"], item.data)
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
}),
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
await Promise.all(promises)
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
export const Errors = {
|
|
216
|
+
NotFound: class extends Error {
|
|
217
|
+
constructor(public id: string) {
|
|
218
|
+
super(`Share not found: ${id}`)
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
InvalidSecret: class extends Error {
|
|
222
|
+
constructor(public id: string) {
|
|
223
|
+
super(`Share secret invalid: ${id}`)
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
AlreadyExists: class extends Error {
|
|
227
|
+
constructor(public id: string) {
|
|
228
|
+
super(`Share already exists: ${id}`)
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { AwsClient } from "aws4fetch"
|
|
2
|
+
import { lazy } from "@neurocode-ai/core/util/lazy"
|
|
3
|
+
|
|
4
|
+
export namespace Storage {
|
|
5
|
+
export interface Adapter {
|
|
6
|
+
read(path: string): Promise<string | undefined>
|
|
7
|
+
write(path: string, value: string): Promise<void>
|
|
8
|
+
remove(path: string): Promise<void>
|
|
9
|
+
list(options?: { prefix?: string; limit?: number; after?: string; before?: string }): Promise<string[]>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createAdapter(client: AwsClient, endpoint: string, bucket: string): Adapter {
|
|
13
|
+
const base = `${endpoint}/${bucket}`
|
|
14
|
+
return {
|
|
15
|
+
async read(path: string): Promise<string | undefined> {
|
|
16
|
+
const response = await client.fetch(`${base}/${path}`)
|
|
17
|
+
if (response.status === 404) return undefined
|
|
18
|
+
if (!response.ok) throw new Error(`Failed to read ${path}: ${response.status}`)
|
|
19
|
+
return response.text()
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
async write(path: string, value: string): Promise<void> {
|
|
23
|
+
const response = await client.fetch(`${base}/${path}`, {
|
|
24
|
+
method: "PUT",
|
|
25
|
+
body: value,
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
if (!response.ok) throw new Error(`Failed to write ${path}: ${response.status}`)
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async remove(path: string): Promise<void> {
|
|
34
|
+
const response = await client.fetch(`${base}/${path}`, {
|
|
35
|
+
method: "DELETE",
|
|
36
|
+
})
|
|
37
|
+
if (!response.ok) throw new Error(`Failed to remove ${path}: ${response.status}`)
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async list(options?: { prefix?: string; limit?: number; after?: string; before?: string }): Promise<string[]> {
|
|
41
|
+
const prefix = options?.prefix || ""
|
|
42
|
+
const params = new URLSearchParams({ "list-type": "2", prefix })
|
|
43
|
+
if (options?.limit) params.set("max-keys", options.limit.toString())
|
|
44
|
+
if (options?.after) {
|
|
45
|
+
const afterPath = prefix + options.after + ".json"
|
|
46
|
+
params.set("start-after", afterPath)
|
|
47
|
+
}
|
|
48
|
+
const response = await client.fetch(`${base}?${params}`)
|
|
49
|
+
if (!response.ok) throw new Error(`Failed to list ${prefix}: ${response.status}`)
|
|
50
|
+
const xml = await response.text()
|
|
51
|
+
const keys: string[] = []
|
|
52
|
+
const regex = /<Key>([^<]+)<\/Key>/g
|
|
53
|
+
let match
|
|
54
|
+
while ((match = regex.exec(xml)) !== null) {
|
|
55
|
+
keys.push(match[1])
|
|
56
|
+
}
|
|
57
|
+
if (options?.before) {
|
|
58
|
+
const beforePath = prefix + options.before + ".json"
|
|
59
|
+
return keys.filter((key) => key < beforePath)
|
|
60
|
+
}
|
|
61
|
+
return keys
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function s3(): Adapter {
|
|
67
|
+
const bucket = process.env.NEUROCODE_STORAGE_BUCKET!
|
|
68
|
+
const region = process.env.NEUROCODE_STORAGE_REGION || "us-east-1"
|
|
69
|
+
const client = new AwsClient({
|
|
70
|
+
region,
|
|
71
|
+
accessKeyId: process.env.NEUROCODE_STORAGE_ACCESS_KEY_ID!,
|
|
72
|
+
secretAccessKey: process.env.NEUROCODE_STORAGE_SECRET_ACCESS_KEY!,
|
|
73
|
+
})
|
|
74
|
+
return createAdapter(client, `https://s3.${region}.amazonaws.com`, bucket)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function r2() {
|
|
78
|
+
const accountId = process.env.NEUROCODE_STORAGE_ACCOUNT_ID!
|
|
79
|
+
const client = new AwsClient({
|
|
80
|
+
accessKeyId: process.env.NEUROCODE_STORAGE_ACCESS_KEY_ID!,
|
|
81
|
+
secretAccessKey: process.env.NEUROCODE_STORAGE_SECRET_ACCESS_KEY!,
|
|
82
|
+
})
|
|
83
|
+
return createAdapter(client, `https://${accountId}.r2.cloudflarestorage.com`, process.env.NEUROCODE_STORAGE_BUCKET!)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const adapter = lazy(() => {
|
|
87
|
+
const type = process.env.NEUROCODE_STORAGE_ADAPTER
|
|
88
|
+
if (type === "r2") return r2()
|
|
89
|
+
if (type === "s3") return s3()
|
|
90
|
+
throw new Error("No storage adapter configured")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
function resolve(key: string[]) {
|
|
94
|
+
return key.join("/") + ".json"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function read<T>(key: string[]) {
|
|
98
|
+
const result = await adapter().read(resolve(key))
|
|
99
|
+
if (!result) return undefined
|
|
100
|
+
return JSON.parse(result) as T
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function write<T>(key: string[], value: T) {
|
|
104
|
+
return adapter().write(resolve(key), JSON.stringify(value))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function remove(key: string[]) {
|
|
108
|
+
return adapter().remove(resolve(key))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function list(options?: { prefix?: string[]; limit?: number; after?: string; before?: string }) {
|
|
112
|
+
const p = options?.prefix ? options.prefix.join("/") + (options.prefix.length ? "/" : "") : ""
|
|
113
|
+
const result = await adapter().list({
|
|
114
|
+
prefix: p,
|
|
115
|
+
limit: options?.limit,
|
|
116
|
+
after: options?.after,
|
|
117
|
+
before: options?.before,
|
|
118
|
+
})
|
|
119
|
+
return result.map((x) => x.replace(/\.json$/, "").split("/"))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function update<T>(key: string[], fn: (draft: T) => void) {
|
|
123
|
+
const val = await read<T>(key)
|
|
124
|
+
if (!val) throw new Error("Not found")
|
|
125
|
+
fn(val)
|
|
126
|
+
await write(key, val)
|
|
127
|
+
return val
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../ui/src/custom-elements.d.ts
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @refresh reload
|
|
2
|
+
import { createHandler, StartServer } from "@solidjs/start/server"
|
|
3
|
+
import { getRequestEvent } from "solid-js/web"
|
|
4
|
+
|
|
5
|
+
export default createHandler(() => (
|
|
6
|
+
<StartServer
|
|
7
|
+
document={({ assets, children, scripts }) => {
|
|
8
|
+
const lang = (() => {
|
|
9
|
+
const event = getRequestEvent()
|
|
10
|
+
const header = event?.request.headers.get("accept-language")
|
|
11
|
+
if (!header) return "en"
|
|
12
|
+
for (const item of header.split(",")) {
|
|
13
|
+
const value = item.trim().split(";")[0]?.toLowerCase()
|
|
14
|
+
if (!value) continue
|
|
15
|
+
if (value.startsWith("zh")) return "zh"
|
|
16
|
+
if (value.startsWith("en")) return "en"
|
|
17
|
+
}
|
|
18
|
+
return "en"
|
|
19
|
+
})()
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<html lang={lang}>
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="utf-8" />
|
|
25
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
26
|
+
<title>OpenCode</title>
|
|
27
|
+
<meta name="theme-color" content="#F8F7F7" />
|
|
28
|
+
{assets}
|
|
29
|
+
</head>
|
|
30
|
+
<body class="antialiased overscroll-none text-12-regular">
|
|
31
|
+
<div id="app">{children}</div>
|
|
32
|
+
{scripts}
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
35
|
+
)
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
))
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { A } from "@solidjs/router"
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<main class="text-center mx-auto text-gray-700 p-4">
|
|
6
|
+
<h1 class="max-6-xs text-6xl text-sky-700 font-thin uppercase my-16">Not Found</h1>
|
|
7
|
+
<p class="mt-8">
|
|
8
|
+
Visit{" "}
|
|
9
|
+
<a href="https://solidjs.com" target="_blank" class="text-sky-600 hover:underline">
|
|
10
|
+
solidjs.com
|
|
11
|
+
</a>{" "}
|
|
12
|
+
to learn how to build Solid apps.
|
|
13
|
+
</p>
|
|
14
|
+
<p class="my-4">
|
|
15
|
+
<A href="/" class="text-sky-600 hover:underline">
|
|
16
|
+
Home
|
|
17
|
+
</A>
|
|
18
|
+
{" - "}
|
|
19
|
+
<A href="/about" class="text-sky-600 hover:underline">
|
|
20
|
+
About Page
|
|
21
|
+
</A>
|
|
22
|
+
</p>
|
|
23
|
+
</main>
|
|
24
|
+
)
|
|
25
|
+
}
|