Просмотр исходного кода

core: refactor share system to separate session IDs from share IDs

- Generate shorter share IDs from session IDs for better URL structure
- Update API routes to use shareID parameter instead of sessionID
- Improve sync mechanism with better data queuing and deduplication
- Maintain backward compatibility while improving security and organization
Dax Raad 2 месяцев назад
Родитель
Сommit
69d1381ba3

+ 9 - 14
packages/enterprise/src/core/share.ts

@@ -8,6 +8,7 @@ export namespace Share {
   export const Info = z.object({
     id: z.string(),
     secret: z.string(),
+    sessionID: z.string(),
   })
   export type Info = z.infer<typeof Info>
 
@@ -28,10 +29,6 @@ export namespace Share {
       type: z.literal("session_diff"),
       data: z.custom<FileDiff[]>(),
     }),
-    z.object({
-      type: z.literal("session_status"),
-      data: z.custom<SessionStatus>(),
-    }),
     z.object({
       type: z.literal("model"),
       data: z.custom<Model[]>(),
@@ -39,9 +36,10 @@ export namespace Share {
   ])
   export type Data = z.infer<typeof Data>
 
-  export const create = fn(Info.pick({ id: true }), async (body) => {
+  export const create = fn(z.object({ sessionID: z.string() }), async (body) => {
     const info: Info = {
-      id: body.id,
+      id: body.sessionID.slice(-8),
+      sessionID: body.sessionID,
       secret: crypto.randomUUID(),
     }
     const exists = await get(info.id)
@@ -51,8 +49,8 @@ export namespace Share {
     return info
   })
 
-  async function get(sessionID: string) {
-    return Storage.read<Info>(["share", sessionID])
+  export async function get(id: string) {
+    return Storage.read<Info>(["share", id])
   }
 
   export const remove = fn(Info.pick({ id: true, secret: true }), async (body) => {
@@ -66,8 +64,8 @@ export namespace Share {
     }
   })
 
-  export async function data(sessionID: string) {
-    const list = await Storage.list(["share_data", sessionID])
+  export async function data(id: string) {
+    const list = await Storage.list(["share_data", id])
     const promises = []
     for (const item of list) {
       promises.push(
@@ -85,7 +83,7 @@ export namespace Share {
 
   export const sync = fn(
     z.object({
-      share: Info,
+      share: Info.pick({ id: true, secret: true }),
       data: Data.array(),
     }),
     async (input) => {
@@ -112,9 +110,6 @@ export namespace Share {
               case "session_diff":
                 await Storage.write(["share_data", input.share.id, "session_diff"], item.data)
                 break
-              case "session_status":
-                await Storage.write(["share_data", input.share.id, "session_status"], item.data)
-                break
               case "model":
                 await Storage.write(["share_data", input.share.id, "model"], item.data)
                 break

+ 0 - 1
packages/enterprise/src/core/storage.ts

@@ -19,7 +19,6 @@ export namespace Storage {
     return {
       async read(path: string): Promise<string | undefined> {
         try {
-          console.log("reading", bucket, path)
           const command = new GetObjectCommand({
             Bucket: bucket,
             Key: path,

+ 15 - 13
packages/enterprise/src/routes/api/[...path].ts

@@ -37,6 +37,7 @@ app
               schema: resolver(
                 z
                   .object({
+                    id: z.string(),
                     url: z.string(),
                     secret: z.string(),
                   })
@@ -50,17 +51,18 @@ app
     validator("json", z.object({ sessionID: z.string() })),
     async (c) => {
       const body = c.req.valid("json")
-      const share = await Share.create({ id: body.sessionID })
+      const share = await Share.create({ sessionID: body.sessionID })
       const protocol = c.req.header("x-forwarded-proto") ?? c.req.header("x-forwarded-protocol") ?? "https"
       const host = c.req.header("x-forwarded-host") ?? c.req.header("host")
       return c.json({
+        id: share.id,
         secret: share.secret,
         url: `${protocol}://${host}/share/${share.id}`,
       })
     },
   )
   .post(
-    "/share/:sessionID/sync",
+    "/share/:shareID/sync",
     describeRoute({
       description: "Sync share data",
       operationId: "share.sync",
@@ -75,20 +77,20 @@ app
         },
       },
     }),
-    validator("param", z.object({ sessionID: z.string() })),
+    validator("param", z.object({ shareID: z.string() })),
     validator("json", z.object({ secret: z.string(), data: Share.Data.array() })),
     async (c) => {
-      const { sessionID } = c.req.valid("param")
+      const { shareID } = c.req.valid("param")
       const body = c.req.valid("json")
       await Share.sync({
-        share: { id: sessionID, secret: body.secret },
+        share: { id: shareID, secret: body.secret },
         data: body.data,
       })
       return c.json({})
     },
   )
   .get(
-    "/share/:sessionID/data",
+    "/share/:shareID/data",
     describeRoute({
       description: "Get share data",
       operationId: "share.data",
@@ -103,14 +105,14 @@ app
         },
       },
     }),
-    validator("param", z.object({ sessionID: z.string() })),
+    validator("param", z.object({ shareID: z.string() })),
     async (c) => {
-      const { sessionID } = c.req.valid("param")
-      return c.json(await Share.data(sessionID))
+      const { shareID } = c.req.valid("param")
+      return c.json(await Share.data(shareID))
     },
   )
   .delete(
-    "/share/:sessionID",
+    "/share/:shareID",
     describeRoute({
       description: "Remove a share",
       operationId: "share.remove",
@@ -125,12 +127,12 @@ app
         },
       },
     }),
-    validator("param", z.object({ sessionID: z.string() })),
+    validator("param", z.object({ shareID: z.string() })),
     validator("json", z.object({ secret: z.string() })),
     async (c) => {
-      const { sessionID } = c.req.valid("param")
+      const { shareID } = c.req.valid("param")
       const body = c.req.valid("json")
-      await Share.remove({ id: sessionID, secret: body.secret })
+      await Share.remove({ id: shareID, secret: body.secret })
       return c.json({})
     },
   )

+ 24 - 22
packages/enterprise/src/routes/share/[sessionID].tsx → packages/enterprise/src/routes/share/[shareID].tsx

@@ -25,9 +25,12 @@ const SessionDataMissingError = NamedError.create(
   }),
 )
 
-const getData = query(async (sessionID) => {
-  const data = await Share.data(sessionID)
+const getData = query(async (shareID) => {
+  const share = await Share.get(shareID)
+  if (!share) throw new SessionDataMissingError({ sessionID: shareID })
+  const data = await Share.data(shareID)
   const result: {
+    sessionID: string
     session: Session[]
     session_diff: {
       [sessionID: string]: FileDiff[]
@@ -45,12 +48,13 @@ const getData = query(async (sessionID) => {
       [sessionID: string]: Model[]
     }
   } = {
+    sessionID: share.sessionID,
     session: [],
     session_diff: {
-      [sessionID]: [],
+      [share.sessionID]: [],
     },
     session_status: {
-      [sessionID]: {
+      [share.sessionID]: {
         type: "idle",
       },
     },
@@ -64,10 +68,7 @@ const getData = query(async (sessionID) => {
         result.session.push(item.data)
         break
       case "session_diff":
-        result.session_diff[sessionID] = item.data
-        break
-      case "session_status":
-        result.session_status[sessionID] = item.data
+        result.session_diff[share.sessionID] = item.data
         break
       case "message":
         result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? []
@@ -78,24 +79,25 @@ const getData = query(async (sessionID) => {
         result.part[item.data.messageID].push(item.data)
         break
       case "model":
-        result.model[sessionID] = item.data
+        result.model[share.sessionID] = item.data
         break
     }
   }
-  const match = Binary.search(result.session, sessionID!, (s) => s.id)
-  if (!match.found) throw new SessionDataMissingError({ sessionID })
+  const match = Binary.search(result.session, share.sessionID, (s) => s.id)
+  if (!match.found) throw new SessionDataMissingError({ sessionID: share.sessionID })
+  console.log(result)
   return result
 }, "getShareData")
 
 export const route = {
-  preload: ({ params }) => getData(params.sessionID),
+  preload: ({ params }) => getData(params.shareID),
 } satisfies RouteDefinition
 
 export default function () {
   const params = useParams()
   const data = createAsync(async () => {
-    if (!params.sessionID) throw new Error("Missing sessionID")
-    return getData(params.sessionID)
+    if (!params.shareID) throw new Error("Missing sessionID")
+    return getData(params.shareID)
   })
 
   return (
@@ -115,12 +117,12 @@ export default function () {
               const [store, setStore] = createStore({
                 messageId: undefined as string | undefined,
               })
-              const match = createMemo(() => Binary.search(data().session, params.sessionID!, (s) => s.id))
-              if (!match().found) throw new Error(`Session ${params.sessionID} not found`)
+              const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id))
+              if (!match().found) throw new Error(`Session ${data().sessionID} not found`)
               const info = createMemo(() => data().session[match().index])
               const messages = createMemo(() =>
-                params.sessionID
-                  ? (data().message[params.sessionID]?.filter((m) => m.role === "user") ?? []).sort(
+                data().sessionID
+                  ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
                       (a, b) => b.time.created - a.time.created,
                     )
                   : [],
@@ -138,8 +140,8 @@ export default function () {
               }
               const provider = createMemo(() => activeMessage()?.model?.providerID)
               const modelID = createMemo(() => activeMessage()?.model?.modelID)
-              const model = createMemo(() => data().model[params.sessionID!]?.find((m) => m.id === modelID()))
-              const diffs = createMemo(() => data().session_diff[params.sessionID!] ?? [])
+              const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
+              const diffs = createMemo(() => data().session_diff[data().sessionID] ?? [])
 
               const title = () => (
                 <div class="flex flex-col gap-4 shrink-0">
@@ -167,7 +169,7 @@ export default function () {
                     <For each={messages()}>
                       {(message) => (
                         <SessionTurn
-                          sessionID={params.sessionID!}
+                          sessionID={data().sessionID}
                           messageID={message.id}
                           classes={{
                             root: "min-w-0 w-full relative",
@@ -254,7 +256,7 @@ export default function () {
                             </>
                           </Show>
                           <SessionTurn
-                            sessionID={params.sessionID!}
+                            sessionID={data().sessionID}
                             messageID={store.messageId ?? firstUserMessage()!.id!}
                             classes={{ root: "grow", content: "flex flex-col justify-between", container: "pb-20" }}
                           >

+ 38 - 18
packages/opencode/src/share/share-next.ts

@@ -1,5 +1,6 @@
 import { Bus } from "@/bus"
 import { Config } from "@/config/config"
+import { ulid } from "ulid"
 import type { ModelsDev } from "@/provider/models"
 import { Provider } from "@/provider/provider"
 import { Session } from "@/session"
@@ -10,6 +11,7 @@ import type * as SDK from "@opencode-ai/sdk"
 
 export namespace ShareNext {
   const log = Log.create({ service: "share-next" })
+
   export async function init() {
     const config = await Config.get()
     if (!config.enterprise) return
@@ -70,11 +72,8 @@ export namespace ShareNext {
       body: JSON.stringify({ sessionID: sessionID }),
     })
       .then((x) => x.json())
-      .then((x) => x as { url: string; secret: string })
-    await Storage.write(["session_share", sessionID], {
-      id: sessionID,
-      ...result,
-    })
+      .then((x) => x as { id: string; url: string; secret: string })
+    await Storage.write(["session_share", sessionID], result)
     fullSync(sessionID)
     return result
   }
@@ -109,20 +108,41 @@ export namespace ShareNext {
         data: ModelsDev.Model[]
       }
 
+  const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
   async function sync(sessionID: string, data: Data[]) {
-    const url = await Config.get().then((x) => x.enterprise!.url)
-    const share = await get(sessionID)
-    if (!share) return
-    await fetch(`${url}/api/share/${share.id}/sync`, {
-      method: "POST",
-      headers: {
-        "Content-Type": "application/json",
-      },
-      body: JSON.stringify({
-        secret: share.secret,
-        data,
-      }),
-    })
+    const existing = queue.get(sessionID)
+    if (existing) {
+      for (const item of data) {
+        existing.data.set("id" in item ? (item.id as string) : ulid(), item)
+      }
+      return
+    }
+
+    const dataMap = new Map<string, Data>()
+    for (const item of data) {
+      dataMap.set("id" in item ? (item.id as string) : ulid(), item)
+    }
+
+    const timeout = setTimeout(async () => {
+      const queued = queue.get(sessionID)
+      if (!queued) return
+      queue.delete(sessionID)
+      const url = await Config.get().then((x) => x.enterprise!.url)
+      const share = await get(sessionID)
+      if (!share) return
+
+      await fetch(`${url}/api/share/${share.id}/sync`, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+          secret: share.secret,
+          data: Array.from(queued.data.values()),
+        }),
+      })
+    }, 1000)
+    queue.set(sessionID, { timeout, data: dataMap })
   }
 
   export async function remove(sessionID: string) {