mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 03:48:39 +00:00
Compare commits
14 Commits
v0.14.0-ca
...
v0.14.0-ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e8fe28326 | ||
|
|
964e475c5f | ||
|
|
905d7d18e4 | ||
|
|
81729703d9 | ||
|
|
f98db24391 | ||
|
|
704532bd2f | ||
|
|
8d342f85ad | ||
|
|
fed2503782 | ||
|
|
236c6e00df | ||
|
|
7584ab4b91 | ||
|
|
b639e52dca | ||
|
|
5d114ea965 | ||
|
|
d015be24e6 | ||
|
|
850bbee629 |
@@ -9,10 +9,10 @@ corepack prepare yarn@stable --activate
|
||||
yarn install
|
||||
|
||||
# Build Server Dependencies
|
||||
yarn workspace @affine/storage build
|
||||
yarn workspace @affine/server-native build
|
||||
|
||||
# Create database
|
||||
yarn workspace @affine/server prisma db push
|
||||
|
||||
# Create user username: affine, password: affine
|
||||
echo "INSERT INTO \"users\"(\"id\",\"name\",\"email\",\"email_verified\",\"created_at\",\"password\") VALUES('99f3ad04-7c9b-441e-a6db-79f73aa64db9','affine','affine@affine.pro','2024-02-26 15:54:16.974','2024-02-26 15:54:16.974+00','\$argon2id\$v=19\$m=19456,t=2,p=1\$esDS3QCHRH0Kmeh87YPm5Q\$9S+jf+xzw2Hicj6nkWltvaaaXX3dQIxAFwCfFa9o38A');" | yarn workspace @affine/server prisma db execute --stdin
|
||||
echo "INSERT INTO \"users\"(\"id\",\"name\",\"email\",\"email_verified\",\"created_at\",\"password\") VALUES('99f3ad04-7c9b-441e-a6db-79f73aa64db9','affine','affine@affine.pro','2024-02-26 15:54:16.974','2024-02-26 15:54:16.974+00','\$argon2id\$v=19\$m=19456,t=2,p=1\$esDS3QCHRH0Kmeh87YPm5Q\$9S+jf+xzw2Hicj6nkWltvaaaXX3dQIxAFwCfFa9o38A');" | yarn workspace @affine/server prisma db execute --stdin
|
||||
|
||||
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
@@ -44,10 +44,10 @@ mod:component:
|
||||
- any-glob-to-any-file:
|
||||
- 'packages/frontend/component/**/*'
|
||||
|
||||
mod:storage:
|
||||
mod:server-native:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'packages/backend/storage/**/*'
|
||||
- 'packages/backend/native/**/*'
|
||||
|
||||
mod:native:
|
||||
- changed-files:
|
||||
|
||||
38
.github/workflows/build-server-image.yml
vendored
38
.github/workflows/build-server-image.yml
vendored
@@ -66,18 +66,18 @@ jobs:
|
||||
path: ./packages/frontend/web/dist
|
||||
if-no-files-found: error
|
||||
|
||||
build-storage:
|
||||
name: Build Storage - ${{ matrix.targets.name }}
|
||||
build-server-native:
|
||||
name: Build Server native - ${{ matrix.targets.name }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
targets:
|
||||
- name: x86_64-unknown-linux-gnu
|
||||
file: storage.node
|
||||
file: server-native.node
|
||||
- name: aarch64-unknown-linux-gnu
|
||||
file: storage.arm64.node
|
||||
file: server-native.arm64.node
|
||||
- name: armv7-unknown-linux-gnueabihf
|
||||
file: storage.armv7.node
|
||||
file: server-native.armv7.node
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -88,18 +88,18 @@ jobs:
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
electron-install: false
|
||||
extra-flags: workspaces focus @affine/storage
|
||||
extra-flags: workspaces focus @affine/server-native
|
||||
- name: Build Rust
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: ${{ matrix.targets.name }}
|
||||
package: '@affine/storage'
|
||||
package: '@affine/server-native'
|
||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
- name: Upload ${{ matrix.targets.file }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.targets.file }}
|
||||
path: ./packages/backend/storage/storage.node
|
||||
path: ./packages/backend/native/server-native.node
|
||||
if-no-files-found: error
|
||||
|
||||
build-docker:
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
needs:
|
||||
- build-server
|
||||
- build-web-selfhost
|
||||
- build-storage
|
||||
- build-server-native
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download server dist
|
||||
@@ -116,25 +116,25 @@ jobs:
|
||||
with:
|
||||
name: server-dist
|
||||
path: ./packages/backend/server/dist
|
||||
- name: Download storage.node
|
||||
- name: Download server-native.node
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storage.node
|
||||
name: server-native.node
|
||||
path: ./packages/backend/server
|
||||
- name: Download storage.node arm64
|
||||
- name: Download server-native.node arm64
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storage.arm64.node
|
||||
path: ./packages/backend/storage
|
||||
- name: Download storage.node arm64
|
||||
name: server-native.arm64.node
|
||||
path: ./packages/backend/native
|
||||
- name: Download server-native.node arm64
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storage.armv7.node
|
||||
name: server-native.armv7.node
|
||||
path: .
|
||||
- name: move storage files
|
||||
- name: move server-native files
|
||||
run: |
|
||||
mv ./packages/backend/storage/storage.node ./packages/backend/server/storage.arm64.node
|
||||
mv storage.node ./packages/backend/server/storage.armv7.node
|
||||
mv ./packages/backend/native/server-native.node ./packages/backend/server/server-native.arm64.node
|
||||
mv server-native.node ./packages/backend/server/server-native.armv7.node
|
||||
- name: Setup env
|
||||
run: |
|
||||
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
|
||||
|
||||
26
.github/workflows/build-test.yml
vendored
26
.github/workflows/build-test.yml
vendored
@@ -241,8 +241,8 @@ jobs:
|
||||
path: ./packages/frontend/native/${{ steps.filename.outputs.filename }}
|
||||
if-no-files-found: error
|
||||
|
||||
build-storage:
|
||||
name: Build Storage
|
||||
build-server-native:
|
||||
name: Build Server native
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_DEBUG: '1'
|
||||
@@ -251,19 +251,19 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/storage
|
||||
extra-flags: workspaces focus @affine/server-native
|
||||
electron-install: false
|
||||
- name: Build Rust
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: 'x86_64-unknown-linux-gnu'
|
||||
package: '@affine/storage'
|
||||
package: '@affine/server-native'
|
||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
- name: Upload storage.node
|
||||
- name: Upload server-native.node
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storage.node
|
||||
path: ./packages/backend/storage/storage.node
|
||||
name: server-native.node
|
||||
path: ./packages/backend/native/server-native.node
|
||||
if-no-files-found: error
|
||||
|
||||
build-web:
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
server-test:
|
||||
name: Server Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-storage
|
||||
needs: build-server-native
|
||||
env:
|
||||
NODE_ENV: test
|
||||
DISTRIBUTION: browser
|
||||
@@ -324,10 +324,10 @@ jobs:
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
|
||||
- name: Download storage.node
|
||||
- name: Download server-native.node
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storage.node
|
||||
name: server-native.node
|
||||
path: ./packages/backend/server
|
||||
|
||||
- name: Initialize database
|
||||
@@ -383,7 +383,7 @@ jobs:
|
||||
yarn workspace @affine/electron build:dev
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-desktop-cloud e2e
|
||||
needs:
|
||||
- build-storage
|
||||
- build-server-native
|
||||
- build-native
|
||||
services:
|
||||
postgres:
|
||||
@@ -411,10 +411,10 @@ jobs:
|
||||
playwright-install: true
|
||||
hard-link-nm: false
|
||||
|
||||
- name: Download storage.node
|
||||
- name: Download server-native.node
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storage.node
|
||||
name: server-native.node
|
||||
path: ./packages/backend/server
|
||||
|
||||
- name: Download affine.linux-x64-gnu.node
|
||||
|
||||
@@ -21,6 +21,6 @@ packages/frontend/templates/onboarding
|
||||
|
||||
# auto-generated by NAPI-RS
|
||||
# fixme(@joooye34): need script to check and generate ignore list here
|
||||
packages/backend/storage/index.d.ts
|
||||
packages/backend/native/index.d.ts
|
||||
packages/frontend/native/index.d.ts
|
||||
packages/frontend/native/index.js
|
||||
|
||||
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -45,10 +45,11 @@ name = "affine_schema"
|
||||
version = "0.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "affine_storage"
|
||||
name = "affine_server_native"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"file-format",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
@@ -434,6 +435,12 @@ version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
|
||||
|
||||
[[package]]
|
||||
name = "file-format"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ba1b81b3c213cf1c071f8bf3b83531f310df99642e58c48247272eef006cae5"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.23"
|
||||
|
||||
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = [
|
||||
"./packages/frontend/native",
|
||||
"./packages/frontend/native/schema",
|
||||
"./packages/backend/storage",
|
||||
"./packages/backend/native",
|
||||
]
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
|
||||
@@ -93,7 +93,7 @@ yarn workspace @affine/native build
|
||||
### Build Server Dependencies
|
||||
|
||||
```sh
|
||||
yarn workspace @affine/storage build
|
||||
yarn workspace @affine/server-native build
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -81,7 +81,7 @@ yarn workspace @affine/server prisma studio
|
||||
|
||||
```
|
||||
# build native
|
||||
yarn workspace @affine/storage build
|
||||
yarn workspace @affine/server-native build
|
||||
yarn workspace @affine/native build
|
||||
```
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"dev:electron": "yarn workspace @affine/electron dev",
|
||||
"build": "yarn nx build @affine/web",
|
||||
"build:electron": "yarn nx build @affine/electron",
|
||||
"build:storage": "yarn nx run-many -t build -p @affine/storage",
|
||||
"build:server-native": "yarn nx run-many -t build -p @affine/server-native",
|
||||
"start:web-static": "yarn workspace @affine/web static-server",
|
||||
"serve:test-static": "yarn exec serve tests/fixtures --cors -p 8081",
|
||||
"lint:eslint": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" eslint . --ext .js,mjs,.ts,.tsx --cache",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "affine_storage"
|
||||
name = "affine_server_native"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -8,6 +8,7 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
file-format = { version = "0.24", features = ["reader"] }
|
||||
napi = { version = "2", default-features = false, features = [
|
||||
"napi5",
|
||||
"async",
|
||||
@@ -1,6 +1,8 @@
|
||||
/* auto-generated by NAPI-RS */
|
||||
/* eslint-disable */
|
||||
|
||||
export function getMime(input: Uint8Array): string
|
||||
|
||||
/**
|
||||
* Merge updates in form like `Y.applyUpdate(doc, update)` way and return the
|
||||
* result binary.
|
||||
@@ -3,9 +3,9 @@ import { createRequire } from 'node:module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/** @type {import('.')} */
|
||||
const binding = require('./storage.node');
|
||||
const binding = require('./server-native.node');
|
||||
|
||||
export const Storage = binding.Storage;
|
||||
export const mergeUpdatesInApplyWay = binding.mergeUpdatesInApplyWay;
|
||||
export const verifyChallengeResponse = binding.verifyChallengeResponse;
|
||||
export const mintChallengeResponse = binding.mintChallengeResponse;
|
||||
export const getMime = binding.getMime;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@affine/storage",
|
||||
"name": "@affine/server-native",
|
||||
"version": "0.14.0",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0 < 11 || >= 11.8.0"
|
||||
@@ -10,13 +10,13 @@
|
||||
"types": "index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./storage.node",
|
||||
"require": "./server-native.node",
|
||||
"import": "./index.js",
|
||||
"types": "./index.d.ts"
|
||||
}
|
||||
},
|
||||
"napi": {
|
||||
"binaryName": "storage",
|
||||
"binaryName": "server-native",
|
||||
"targets": [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
@@ -29,10 +29,7 @@
|
||||
"scripts": {
|
||||
"test": "node --test ./__tests__/**/*.spec.js",
|
||||
"build": "napi build --release --strip --no-const-enum",
|
||||
"build:debug": "napi build",
|
||||
"prepublishOnly": "napi prepublish -t npm",
|
||||
"artifacts": "napi artifacts",
|
||||
"version": "napi version"
|
||||
"build:debug": "napi build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.0.0-alpha.46",
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@affine/storage",
|
||||
"name": "@affine/server-native",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"root": "packages/backend/storage",
|
||||
"sourceRoot": "packages/backend/storage/src",
|
||||
"root": "packages/backend/native",
|
||||
"sourceRoot": "packages/backend/native/src",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "nx:run-script",
|
||||
8
packages/backend/native/src/file_type.rs
Normal file
8
packages/backend/native/src/file_type.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use napi_derive::napi;
|
||||
|
||||
#[napi]
|
||||
pub fn get_mime(input: &[u8]) -> String {
|
||||
file_format::FileFormat::from_bytes(input)
|
||||
.media_type()
|
||||
.to_string()
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#![deny(clippy::all)]
|
||||
|
||||
pub mod file_type;
|
||||
pub mod hashcash;
|
||||
|
||||
use std::fmt::{Debug, Display};
|
||||
@@ -11,7 +11,7 @@ yarn
|
||||
### Build Native binding
|
||||
|
||||
```bash
|
||||
yarn workspace @affine/storage build
|
||||
yarn workspace @affine/server-native build
|
||||
```
|
||||
|
||||
### Run server
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-cli": "^7.4.1",
|
||||
"express": "^4.19.2",
|
||||
"file-type": "^19.0.0",
|
||||
"get-stream": "^9.0.1",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
@@ -96,7 +95,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@affine/storage": "workspace:*",
|
||||
"@affine/server-native": "workspace:*",
|
||||
"@napi-rs/image": "^1.9.1",
|
||||
"@nestjs/testing": "^10.3.7",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
|
||||
@@ -45,6 +45,7 @@ if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
|
||||
|
||||
AFFiNE.plugins.use('copilot', {
|
||||
openai: {},
|
||||
fal: {},
|
||||
});
|
||||
AFFiNE.plugins.use('redis');
|
||||
AFFiNE.plugins.use('payment', {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
let storageModule: typeof import('@affine/storage');
|
||||
let serverNativeModule: typeof import('@affine/server-native');
|
||||
try {
|
||||
storageModule = await import('@affine/storage');
|
||||
serverNativeModule = await import('@affine/server-native');
|
||||
} catch {
|
||||
const require = createRequire(import.meta.url);
|
||||
storageModule =
|
||||
serverNativeModule =
|
||||
process.arch === 'arm64'
|
||||
? require('../../../storage.arm64.node')
|
||||
? require('../../../server-native.arm64.node')
|
||||
: process.arch === 'arm'
|
||||
? require('../../../storage.armv7.node')
|
||||
: require('../../../storage.node');
|
||||
? require('../../../server-native.armv7.node')
|
||||
: require('../../../server-native.node');
|
||||
}
|
||||
|
||||
export const mergeUpdatesInApplyWay = storageModule.mergeUpdatesInApplyWay;
|
||||
export const mergeUpdatesInApplyWay = serverNativeModule.mergeUpdatesInApplyWay;
|
||||
|
||||
export const verifyChallengeResponse = async (
|
||||
response: any,
|
||||
@@ -21,10 +21,12 @@ export const verifyChallengeResponse = async (
|
||||
resource: string
|
||||
) => {
|
||||
if (typeof response !== 'string' || !response || !resource) return false;
|
||||
return storageModule.verifyChallengeResponse(response, bits, resource);
|
||||
return serverNativeModule.verifyChallengeResponse(response, bits, resource);
|
||||
};
|
||||
|
||||
export const mintChallengeResponse = async (resource: string, bits: number) => {
|
||||
if (!resource) return null;
|
||||
return storageModule.mintChallengeResponse(resource, bits);
|
||||
return serverNativeModule.mintChallengeResponse(resource, bits);
|
||||
};
|
||||
|
||||
export const getMime = serverNativeModule.getMime;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
import { crc32 } from '@node-rs/crc32';
|
||||
import { fileTypeFromBuffer } from 'file-type';
|
||||
import { getStreamAsBuffer } from 'get-stream';
|
||||
|
||||
import { getMime } from '../native';
|
||||
import { BlobInputType, PutObjectMetadata } from './provider';
|
||||
|
||||
export async function toBuffer(input: BlobInputType): Promise<Buffer> {
|
||||
@@ -35,8 +35,7 @@ export async function autoMetadata(
|
||||
// mime type
|
||||
if (!metadata.contentType) {
|
||||
try {
|
||||
const typeResult = await fileTypeFromBuffer(blob);
|
||||
metadata.contentType = typeResult?.mime ?? 'application/octet-stream';
|
||||
metadata.contentType = getMime(blob);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -42,6 +42,11 @@ export interface ChatEvent {
|
||||
data: string;
|
||||
}
|
||||
|
||||
type CheckResult = {
|
||||
model: string | undefined;
|
||||
hasAttachment?: boolean;
|
||||
};
|
||||
|
||||
@Controller('/api/copilot')
|
||||
export class CopilotController {
|
||||
private readonly logger = new Logger(CopilotController.name);
|
||||
@@ -53,17 +58,26 @@ export class CopilotController {
|
||||
private readonly storage: CopilotStorage
|
||||
) {}
|
||||
|
||||
private async hasAttachment(sessionId: string, messageId: string) {
|
||||
private async checkRequest(
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
messageId?: string
|
||||
): Promise<CheckResult> {
|
||||
await this.chatSession.checkQuota(userId);
|
||||
const session = await this.chatSession.get(sessionId);
|
||||
if (!session) {
|
||||
if (!session || session.config.userId !== userId) {
|
||||
throw new BadRequestException('Session not found');
|
||||
}
|
||||
|
||||
const message = await session.getMessageById(messageId);
|
||||
if (Array.isArray(message.attachments) && message.attachments.length) {
|
||||
return true;
|
||||
const ret: CheckResult = { model: session.model };
|
||||
|
||||
if (messageId) {
|
||||
const message = await session.getMessageById(messageId);
|
||||
ret.hasAttachment =
|
||||
Array.isArray(message.attachments) && !!message.attachments.length;
|
||||
}
|
||||
return false;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private async appendSessionMessage(
|
||||
@@ -86,6 +100,17 @@ export class CopilotController {
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
private parseNumber(value: string | string[] | undefined) {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const num = Number.parseInt(Array.isArray(value) ? value[0] : value, 10);
|
||||
if (Number.isNaN(num)) {
|
||||
return undefined;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
private handleError(err: any) {
|
||||
if (err instanceof Error) {
|
||||
const ret = {
|
||||
@@ -107,9 +132,7 @@ export class CopilotController {
|
||||
@Query('messageId') messageId: string,
|
||||
@Query() params: Record<string, string | string[]>
|
||||
): Promise<string> {
|
||||
await this.chatSession.checkQuota(user.id);
|
||||
|
||||
const model = await this.chatSession.get(sessionId).then(s => s?.model);
|
||||
const { model } = await this.checkRequest(user.id, sessionId);
|
||||
const provider = this.provider.getProviderByCapability(
|
||||
CopilotCapability.TextToText,
|
||||
model
|
||||
@@ -155,60 +178,58 @@ export class CopilotController {
|
||||
@Query() params: Record<string, string>
|
||||
): Promise<Observable<ChatEvent>> {
|
||||
try {
|
||||
await this.chatSession.checkQuota(user.id);
|
||||
const { model } = await this.checkRequest(user.id, sessionId);
|
||||
const provider = this.provider.getProviderByCapability(
|
||||
CopilotCapability.TextToText,
|
||||
model
|
||||
);
|
||||
if (!provider) {
|
||||
throw new InternalServerErrorException('No provider available');
|
||||
}
|
||||
|
||||
const session = await this.appendSessionMessage(sessionId, messageId);
|
||||
delete params.messageId;
|
||||
|
||||
return from(
|
||||
provider.generateTextStream(session.finish(params), session.model, {
|
||||
signal: this.getSignal(req),
|
||||
user: user.id,
|
||||
})
|
||||
).pipe(
|
||||
connect(shared$ =>
|
||||
merge(
|
||||
// actual chat event stream
|
||||
shared$.pipe(
|
||||
map(data => ({ type: 'message' as const, id: messageId, data }))
|
||||
),
|
||||
// save the generated text to the session
|
||||
shared$.pipe(
|
||||
toArray(),
|
||||
concatMap(values => {
|
||||
session.push({
|
||||
role: 'assistant',
|
||||
content: values.join(''),
|
||||
createdAt: new Date(),
|
||||
});
|
||||
return from(session.save());
|
||||
}),
|
||||
switchMap(() => EMPTY)
|
||||
)
|
||||
)
|
||||
),
|
||||
catchError(err =>
|
||||
of({
|
||||
type: 'error' as const,
|
||||
data: this.handleError(err),
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
return of({
|
||||
type: 'error' as const,
|
||||
data: this.handleError(err),
|
||||
});
|
||||
}
|
||||
|
||||
const model = await this.chatSession.get(sessionId).then(s => s?.model);
|
||||
const provider = this.provider.getProviderByCapability(
|
||||
CopilotCapability.TextToText,
|
||||
model
|
||||
);
|
||||
if (!provider) {
|
||||
throw new InternalServerErrorException('No provider available');
|
||||
}
|
||||
|
||||
const session = await this.appendSessionMessage(sessionId, messageId);
|
||||
delete params.messageId;
|
||||
|
||||
return from(
|
||||
provider.generateTextStream(session.finish(params), session.model, {
|
||||
signal: this.getSignal(req),
|
||||
user: user.id,
|
||||
})
|
||||
).pipe(
|
||||
connect(shared$ =>
|
||||
merge(
|
||||
// actual chat event stream
|
||||
shared$.pipe(
|
||||
map(data => ({ type: 'message' as const, id: sessionId, data }))
|
||||
),
|
||||
// save the generated text to the session
|
||||
shared$.pipe(
|
||||
toArray(),
|
||||
concatMap(values => {
|
||||
session.push({
|
||||
role: 'assistant',
|
||||
content: values.join(''),
|
||||
createdAt: new Date(),
|
||||
});
|
||||
return from(session.save());
|
||||
}),
|
||||
switchMap(() => EMPTY)
|
||||
)
|
||||
)
|
||||
),
|
||||
catchError(err =>
|
||||
of({
|
||||
type: 'error' as const,
|
||||
data: this.handleError(err),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Sse('/chat/:sessionId/images')
|
||||
@@ -220,75 +241,77 @@ export class CopilotController {
|
||||
@Query() params: Record<string, string>
|
||||
): Promise<Observable<ChatEvent>> {
|
||||
try {
|
||||
await this.chatSession.checkQuota(user.id);
|
||||
const { model, hasAttachment } = await this.checkRequest(
|
||||
user.id,
|
||||
sessionId,
|
||||
messageId
|
||||
);
|
||||
const provider = this.provider.getProviderByCapability(
|
||||
hasAttachment
|
||||
? CopilotCapability.ImageToImage
|
||||
: CopilotCapability.TextToImage,
|
||||
model
|
||||
);
|
||||
if (!provider) {
|
||||
throw new InternalServerErrorException('No provider available');
|
||||
}
|
||||
|
||||
const session = await this.appendSessionMessage(sessionId, messageId);
|
||||
delete params.messageId;
|
||||
|
||||
const handleRemoteLink = this.storage.handleRemoteLink.bind(
|
||||
this.storage,
|
||||
user.id,
|
||||
sessionId
|
||||
);
|
||||
|
||||
return from(
|
||||
provider.generateImagesStream(session.finish(params), session.model, {
|
||||
seed: this.parseNumber(params.seed),
|
||||
signal: this.getSignal(req),
|
||||
user: user.id,
|
||||
})
|
||||
).pipe(
|
||||
mergeMap(handleRemoteLink),
|
||||
connect(shared$ =>
|
||||
merge(
|
||||
// actual chat event stream
|
||||
shared$.pipe(
|
||||
map(attachment => ({
|
||||
type: 'attachment' as const,
|
||||
id: messageId,
|
||||
data: attachment,
|
||||
}))
|
||||
),
|
||||
// save the generated text to the session
|
||||
shared$.pipe(
|
||||
toArray(),
|
||||
concatMap(attachments => {
|
||||
session.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
attachments: attachments,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
return from(session.save());
|
||||
}),
|
||||
switchMap(() => EMPTY)
|
||||
)
|
||||
)
|
||||
),
|
||||
catchError(err =>
|
||||
of({
|
||||
type: 'error' as const,
|
||||
data: this.handleError(err),
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
return of({
|
||||
type: 'error' as const,
|
||||
data: this.handleError(err),
|
||||
});
|
||||
}
|
||||
|
||||
const hasAttachment = await this.hasAttachment(sessionId, messageId);
|
||||
const model = await this.chatSession.get(sessionId).then(s => s?.model);
|
||||
const provider = this.provider.getProviderByCapability(
|
||||
hasAttachment
|
||||
? CopilotCapability.ImageToImage
|
||||
: CopilotCapability.TextToImage,
|
||||
model
|
||||
);
|
||||
if (!provider) {
|
||||
throw new InternalServerErrorException('No provider available');
|
||||
}
|
||||
|
||||
const session = await this.appendSessionMessage(sessionId, messageId);
|
||||
delete params.messageId;
|
||||
|
||||
const handleRemoteLink = this.storage.handleRemoteLink.bind(
|
||||
this.storage,
|
||||
user.id,
|
||||
sessionId
|
||||
);
|
||||
|
||||
return from(
|
||||
provider.generateImagesStream(session.finish(params), session.model, {
|
||||
signal: this.getSignal(req),
|
||||
user: user.id,
|
||||
})
|
||||
).pipe(
|
||||
mergeMap(handleRemoteLink),
|
||||
connect(shared$ =>
|
||||
merge(
|
||||
// actual chat event stream
|
||||
shared$.pipe(
|
||||
map(attachment => ({
|
||||
type: 'attachment' as const,
|
||||
id: sessionId,
|
||||
data: attachment,
|
||||
}))
|
||||
),
|
||||
// save the generated text to the session
|
||||
shared$.pipe(
|
||||
toArray(),
|
||||
concatMap(attachments => {
|
||||
session.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
attachments: attachments,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
return from(session.save());
|
||||
}),
|
||||
switchMap(() => EMPTY)
|
||||
)
|
||||
)
|
||||
),
|
||||
catchError(err =>
|
||||
of({
|
||||
type: 'error' as const,
|
||||
data: this.handleError(err),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/unsplash/photos')
|
||||
|
||||
@@ -193,11 +193,12 @@ export class PromptService {
|
||||
return null;
|
||||
}
|
||||
|
||||
async set(name: string, messages: PromptMessage[]) {
|
||||
async set(name: string, model: string, messages: PromptMessage[]) {
|
||||
return await this.db.aiPrompt
|
||||
.create({
|
||||
data: {
|
||||
name,
|
||||
model,
|
||||
messages: {
|
||||
create: messages.map((m, idx) => ({
|
||||
idx,
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert';
|
||||
|
||||
import {
|
||||
CopilotCapability,
|
||||
CopilotImageOptions,
|
||||
CopilotImageToImageProvider,
|
||||
CopilotProviderType,
|
||||
CopilotTextToImageProvider,
|
||||
@@ -41,6 +42,10 @@ export class FalProvider
|
||||
return !!config.apiKey;
|
||||
}
|
||||
|
||||
get type(): CopilotProviderType {
|
||||
return FalProvider.type;
|
||||
}
|
||||
|
||||
getCapabilities(): CopilotCapability[] {
|
||||
return FalProvider.capabilities;
|
||||
}
|
||||
@@ -53,10 +58,7 @@ export class FalProvider
|
||||
async generateImages(
|
||||
messages: PromptMessage[],
|
||||
model: string = this.availableModels[0],
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
options: CopilotImageOptions = {}
|
||||
): Promise<Array<string>> {
|
||||
const { content, attachments } = messages.pop() || {};
|
||||
if (!this.availableModels.includes(model)) {
|
||||
@@ -78,7 +80,7 @@ export class FalProvider
|
||||
image_url: attachments?.[0],
|
||||
prompt: content,
|
||||
sync_mode: true,
|
||||
seed: 42,
|
||||
seed: options.seed || 42,
|
||||
enable_safety_checks: false,
|
||||
}),
|
||||
signal: options.signal,
|
||||
@@ -96,10 +98,7 @@ export class FalProvider
|
||||
async *generateImagesStream(
|
||||
messages: PromptMessage[],
|
||||
model: string = this.availableModels[0],
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
options: CopilotImageOptions = {}
|
||||
): AsyncIterable<string> {
|
||||
const ret = await this.generateImages(messages, model, options);
|
||||
for (const url of ret) {
|
||||
|
||||
@@ -5,6 +5,9 @@ import { ClientOptions, OpenAI } from 'openai';
|
||||
import {
|
||||
ChatMessageRole,
|
||||
CopilotCapability,
|
||||
CopilotChatOptions,
|
||||
CopilotEmbeddingOptions,
|
||||
CopilotImageOptions,
|
||||
CopilotImageToTextProvider,
|
||||
CopilotProviderType,
|
||||
CopilotTextToEmbeddingProvider,
|
||||
@@ -13,7 +16,7 @@ import {
|
||||
PromptMessage,
|
||||
} from '../types';
|
||||
|
||||
const DEFAULT_DIMENSIONS = 256;
|
||||
export const DEFAULT_DIMENSIONS = 256;
|
||||
|
||||
const SIMPLE_IMAGE_URL_REGEX = /^(https?:\/\/|data:image\/)/;
|
||||
|
||||
@@ -59,6 +62,10 @@ export class OpenAIProvider
|
||||
return !!config.apiKey;
|
||||
}
|
||||
|
||||
get type(): CopilotProviderType {
|
||||
return OpenAIProvider.type;
|
||||
}
|
||||
|
||||
getCapabilities(): CopilotCapability[] {
|
||||
return OpenAIProvider.capabilities;
|
||||
}
|
||||
@@ -67,7 +74,7 @@ export class OpenAIProvider
|
||||
return this.availableModels.includes(model);
|
||||
}
|
||||
|
||||
private chatToGPTMessage(
|
||||
protected chatToGPTMessage(
|
||||
messages: PromptMessage[]
|
||||
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
|
||||
// filter redundant fields
|
||||
@@ -92,7 +99,7 @@ export class OpenAIProvider
|
||||
});
|
||||
}
|
||||
|
||||
private checkParams({
|
||||
protected checkParams({
|
||||
messages,
|
||||
embeddings,
|
||||
model,
|
||||
@@ -143,12 +150,7 @@ export class OpenAIProvider
|
||||
async generateText(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'gpt-3.5-turbo',
|
||||
options: {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
options: CopilotChatOptions = {}
|
||||
): Promise<string> {
|
||||
this.checkParams({ messages, model });
|
||||
const result = await this.instance.chat.completions.create(
|
||||
@@ -171,12 +173,7 @@ export class OpenAIProvider
|
||||
async *generateTextStream(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'gpt-3.5-turbo',
|
||||
options: {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
options: CopilotChatOptions = {}
|
||||
): AsyncIterable<string> {
|
||||
this.checkParams({ messages, model });
|
||||
const result = await this.instance.chat.completions.create(
|
||||
@@ -210,11 +207,7 @@ export class OpenAIProvider
|
||||
async generateEmbedding(
|
||||
messages: string | string[],
|
||||
model: string,
|
||||
options: {
|
||||
dimensions: number;
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = { dimensions: DEFAULT_DIMENSIONS }
|
||||
options: CopilotEmbeddingOptions = { dimensions: DEFAULT_DIMENSIONS }
|
||||
): Promise<number[][]> {
|
||||
messages = Array.isArray(messages) ? messages : [messages];
|
||||
this.checkParams({ embeddings: messages, model });
|
||||
@@ -232,10 +225,7 @@ export class OpenAIProvider
|
||||
async generateImages(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'dall-e-3',
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
options: CopilotImageOptions = {}
|
||||
): Promise<Array<string>> {
|
||||
const { content: prompt } = messages.pop() || {};
|
||||
if (!prompt) {
|
||||
@@ -257,10 +247,7 @@ export class OpenAIProvider
|
||||
async *generateImagesStream(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'dall-e-3',
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
options: CopilotImageOptions = {}
|
||||
): AsyncIterable<string> {
|
||||
const ret = await this.generateImages(messages, model, options);
|
||||
for (const url of ret) {
|
||||
|
||||
@@ -278,7 +278,9 @@ export class CopilotResolver {
|
||||
return new TooManyRequestsException('Server is busy');
|
||||
}
|
||||
const session = await this.chatSession.get(options.sessionId);
|
||||
if (!session) return new BadRequestException('Session not found');
|
||||
if (!session || session.config.userId !== user.id) {
|
||||
return new BadRequestException('Session not found');
|
||||
}
|
||||
|
||||
if (options.blobs) {
|
||||
options.attachments = options.attachments || [];
|
||||
|
||||
@@ -81,7 +81,7 @@ export class ChatSession implements AsyncDisposable {
|
||||
}
|
||||
|
||||
pop() {
|
||||
this.state.messages.pop();
|
||||
return this.state.messages.pop();
|
||||
}
|
||||
|
||||
private takeMessages(): ChatMessage[] {
|
||||
@@ -115,7 +115,7 @@ export class ChatSession implements AsyncDisposable {
|
||||
Object.keys(params).length ? params : messages[0]?.params || {},
|
||||
this.config.sessionId
|
||||
),
|
||||
...messages.filter(m => m.content || m.attachments?.length),
|
||||
...messages.filter(m => m.content?.trim() || m.attachments?.length),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface CopilotConfig {
|
||||
openai: OpenAIClientOptions;
|
||||
fal: FalConfig;
|
||||
unsplashKey: string;
|
||||
test: never;
|
||||
}
|
||||
|
||||
export enum AvailableModels {
|
||||
@@ -130,6 +131,8 @@ export type ListHistoriesOptions = {
|
||||
export enum CopilotProviderType {
|
||||
FAL = 'fal',
|
||||
OpenAI = 'openai',
|
||||
// only for test
|
||||
Test = 'test',
|
||||
}
|
||||
|
||||
export enum CopilotCapability {
|
||||
@@ -140,7 +143,34 @@ export enum CopilotCapability {
|
||||
ImageToText = 'image-to-text',
|
||||
}
|
||||
|
||||
const CopilotProviderOptionsSchema = z.object({
|
||||
signal: z.instanceof(AbortSignal).optional(),
|
||||
user: z.string().optional(),
|
||||
});
|
||||
|
||||
const CopilotChatOptionsSchema = CopilotProviderOptionsSchema.extend({
|
||||
temperature: z.number().optional(),
|
||||
maxTokens: z.number().optional(),
|
||||
}).optional();
|
||||
|
||||
export type CopilotChatOptions = z.infer<typeof CopilotChatOptionsSchema>;
|
||||
|
||||
const CopilotEmbeddingOptionsSchema = CopilotProviderOptionsSchema.extend({
|
||||
dimensions: z.number(),
|
||||
}).optional();
|
||||
|
||||
export type CopilotEmbeddingOptions = z.infer<
|
||||
typeof CopilotEmbeddingOptionsSchema
|
||||
>;
|
||||
|
||||
const CopilotImageOptionsSchema = CopilotProviderOptionsSchema.extend({
|
||||
seed: z.number().optional(),
|
||||
}).optional();
|
||||
|
||||
export type CopilotImageOptions = z.infer<typeof CopilotImageOptionsSchema>;
|
||||
|
||||
export interface CopilotProvider {
|
||||
readonly type: CopilotProviderType;
|
||||
getCapabilities(): CopilotCapability[];
|
||||
isModelAvailable(model: string): boolean;
|
||||
}
|
||||
@@ -149,22 +179,12 @@ export interface CopilotTextToTextProvider extends CopilotProvider {
|
||||
generateText(
|
||||
messages: PromptMessage[],
|
||||
model?: string,
|
||||
options?: {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
}
|
||||
options?: CopilotChatOptions
|
||||
): Promise<string>;
|
||||
generateTextStream(
|
||||
messages: PromptMessage[],
|
||||
model?: string,
|
||||
options?: {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
}
|
||||
options?: CopilotChatOptions
|
||||
): AsyncIterable<string>;
|
||||
}
|
||||
|
||||
@@ -172,11 +192,7 @@ export interface CopilotTextToEmbeddingProvider extends CopilotProvider {
|
||||
generateEmbedding(
|
||||
messages: string[] | string,
|
||||
model: string,
|
||||
options: {
|
||||
dimensions: number;
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
}
|
||||
options?: CopilotEmbeddingOptions
|
||||
): Promise<number[][]>;
|
||||
}
|
||||
|
||||
@@ -184,18 +200,12 @@ export interface CopilotTextToImageProvider extends CopilotProvider {
|
||||
generateImages(
|
||||
messages: PromptMessage[],
|
||||
model: string,
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
}
|
||||
options?: CopilotImageOptions
|
||||
): Promise<Array<string>>;
|
||||
generateImagesStream(
|
||||
messages: PromptMessage[],
|
||||
model?: string,
|
||||
options?: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
}
|
||||
options?: CopilotImageOptions
|
||||
): AsyncIterable<string>;
|
||||
}
|
||||
|
||||
@@ -203,22 +213,12 @@ export interface CopilotImageToTextProvider extends CopilotProvider {
|
||||
generateText(
|
||||
messages: PromptMessage[],
|
||||
model: string,
|
||||
options: {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
}
|
||||
options?: CopilotChatOptions
|
||||
): Promise<string>;
|
||||
generateTextStream(
|
||||
messages: PromptMessage[],
|
||||
model: string,
|
||||
options: {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
}
|
||||
options?: CopilotChatOptions
|
||||
): AsyncIterable<string>;
|
||||
}
|
||||
|
||||
@@ -226,18 +226,12 @@ export interface CopilotImageToImageProvider extends CopilotProvider {
|
||||
generateImages(
|
||||
messages: PromptMessage[],
|
||||
model: string,
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
}
|
||||
options?: CopilotImageOptions
|
||||
): Promise<Array<string>>;
|
||||
generateImagesStream(
|
||||
messages: PromptMessage[],
|
||||
model?: string,
|
||||
options?: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
}
|
||||
options?: CopilotImageOptions
|
||||
): AsyncIterable<string>;
|
||||
}
|
||||
|
||||
|
||||
382
packages/backend/server/tests/copilot.e2e.ts
Normal file
382
packages/backend/server/tests/copilot.e2e.ts
Normal file
@@ -0,0 +1,382 @@
|
||||
/// <reference types="../src/global.d.ts" />
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { AuthService } from '../src/core/auth';
|
||||
import { WorkspaceModule } from '../src/core/workspaces';
|
||||
import { ConfigModule } from '../src/fundamentals/config';
|
||||
import { CopilotModule } from '../src/plugins/copilot';
|
||||
import { PromptService } from '../src/plugins/copilot/prompt';
|
||||
import {
|
||||
CopilotProviderService,
|
||||
registerCopilotProvider,
|
||||
} from '../src/plugins/copilot/providers';
|
||||
import { CopilotStorage } from '../src/plugins/copilot/storage';
|
||||
import {
|
||||
acceptInviteById,
|
||||
createTestingApp,
|
||||
createWorkspace,
|
||||
inviteUser,
|
||||
signUp,
|
||||
} from './utils';
|
||||
import {
|
||||
chatWithImages,
|
||||
chatWithText,
|
||||
chatWithTextStream,
|
||||
createCopilotMessage,
|
||||
createCopilotSession,
|
||||
getHistories,
|
||||
MockCopilotTestProvider,
|
||||
textToEventStream,
|
||||
} from './utils/copilot';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
auth: AuthService;
|
||||
app: INestApplication;
|
||||
prompt: PromptService;
|
||||
provider: CopilotProviderService;
|
||||
storage: CopilotStorage;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
const { app } = await createTestingApp({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
plugins: {
|
||||
copilot: {
|
||||
openai: {
|
||||
apiKey: '1',
|
||||
},
|
||||
fal: {
|
||||
apiKey: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
WorkspaceModule,
|
||||
CopilotModule,
|
||||
],
|
||||
});
|
||||
|
||||
const auth = app.get(AuthService);
|
||||
const prompt = app.get(PromptService);
|
||||
const storage = app.get(CopilotStorage);
|
||||
|
||||
t.context.app = app;
|
||||
t.context.auth = auth;
|
||||
t.context.prompt = prompt;
|
||||
t.context.storage = storage;
|
||||
});
|
||||
|
||||
let token: string;
|
||||
const promptName = 'prompt';
|
||||
test.beforeEach(async t => {
|
||||
const { app, prompt } = t.context;
|
||||
const user = await signUp(app, 'test', 'darksky@affine.pro', '123456');
|
||||
token = user.token.token;
|
||||
|
||||
registerCopilotProvider(MockCopilotTestProvider);
|
||||
|
||||
await prompt.set(promptName, 'test', [
|
||||
{ role: 'system', content: 'hello {{word}}' },
|
||||
]);
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
// ==================== session ====================
|
||||
|
||||
test('should create session correctly', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const assertCreateSession = async (
|
||||
workspaceId: string,
|
||||
error: string,
|
||||
asserter = async (x: any) => {
|
||||
t.truthy(await x, error);
|
||||
}
|
||||
) => {
|
||||
await asserter(
|
||||
createCopilotSession(app, token, workspaceId, randomUUID(), promptName)
|
||||
);
|
||||
};
|
||||
|
||||
{
|
||||
const { id } = await createWorkspace(app, token);
|
||||
await assertCreateSession(
|
||||
id,
|
||||
'should be able to create session with cloud workspace that user can access'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
await assertCreateSession(
|
||||
randomUUID(),
|
||||
'should be able to create session with local workspace'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const {
|
||||
token: { token },
|
||||
} = await signUp(app, 'test', 'test@affine.pro', '123456');
|
||||
const { id } = await createWorkspace(app, token);
|
||||
await assertCreateSession(id, '', async x => {
|
||||
await t.throwsAsync(
|
||||
x,
|
||||
{ instanceOf: Error },
|
||||
'should not able to create session with cloud workspace that user cannot access'
|
||||
);
|
||||
});
|
||||
|
||||
const inviteId = await inviteUser(
|
||||
app,
|
||||
token,
|
||||
id,
|
||||
'darksky@affine.pro',
|
||||
'Admin'
|
||||
);
|
||||
await acceptInviteById(app, id, inviteId, false);
|
||||
await assertCreateSession(
|
||||
id,
|
||||
'should able to create session after user have permission'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should be able to use test provider', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const { id } = await createWorkspace(app, token);
|
||||
t.truthy(
|
||||
await createCopilotSession(app, token, id, randomUUID(), promptName),
|
||||
'failed to create session'
|
||||
);
|
||||
});
|
||||
|
||||
// ==================== message ====================
|
||||
|
||||
test('should create message correctly', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
{
|
||||
const { id } = await createWorkspace(app, token);
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
token,
|
||||
id,
|
||||
randomUUID(),
|
||||
promptName
|
||||
);
|
||||
const messageId = await createCopilotMessage(app, token, sessionId);
|
||||
t.truthy(messageId, 'should be able to create message with valid session');
|
||||
}
|
||||
|
||||
{
|
||||
await t.throwsAsync(
|
||||
createCopilotMessage(app, token, randomUUID()),
|
||||
{ instanceOf: Error },
|
||||
'should not able to create message with invalid session'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== chat ====================
|
||||
|
||||
test('should be able to chat with api', async t => {
|
||||
const { app, storage } = t.context;
|
||||
|
||||
Sinon.stub(storage, 'handleRemoteLink').resolvesArg(2);
|
||||
|
||||
const { id } = await createWorkspace(app, token);
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
token,
|
||||
id,
|
||||
randomUUID(),
|
||||
promptName
|
||||
);
|
||||
const messageId = await createCopilotMessage(app, token, sessionId);
|
||||
const ret = await chatWithText(app, token, sessionId, messageId);
|
||||
t.is(ret, 'generate text to text', 'should be able to chat with text');
|
||||
|
||||
const ret2 = await chatWithTextStream(app, token, sessionId, messageId);
|
||||
t.is(
|
||||
ret2,
|
||||
textToEventStream('generate text to text stream', messageId),
|
||||
'should be able to chat with text stream'
|
||||
);
|
||||
|
||||
const ret3 = await chatWithImages(app, token, sessionId, messageId);
|
||||
t.is(
|
||||
ret3,
|
||||
textToEventStream(
|
||||
['https://example.com/image.jpg'],
|
||||
messageId,
|
||||
'attachment'
|
||||
),
|
||||
'should be able to chat with images'
|
||||
);
|
||||
|
||||
Sinon.restore();
|
||||
});
|
||||
|
||||
test('should reject message from different session', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const { id } = await createWorkspace(app, token);
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
token,
|
||||
id,
|
||||
randomUUID(),
|
||||
promptName
|
||||
);
|
||||
const anotherSessionId = await createCopilotSession(
|
||||
app,
|
||||
token,
|
||||
id,
|
||||
randomUUID(),
|
||||
promptName
|
||||
);
|
||||
const anotherMessageId = await createCopilotMessage(
|
||||
app,
|
||||
token,
|
||||
anotherSessionId
|
||||
);
|
||||
await t.throwsAsync(
|
||||
chatWithText(app, token, sessionId, anotherMessageId),
|
||||
{ instanceOf: Error },
|
||||
'should reject message from different session'
|
||||
);
|
||||
});
|
||||
|
||||
test('should reject request from different user', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const { id } = await createWorkspace(app, token);
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
token,
|
||||
id,
|
||||
randomUUID(),
|
||||
promptName
|
||||
);
|
||||
|
||||
// should reject message from different user
|
||||
{
|
||||
const { token } = await signUp(app, 'a1', 'a1@affine.pro', '123456');
|
||||
await t.throwsAsync(
|
||||
createCopilotMessage(app, token.token, sessionId),
|
||||
{ instanceOf: Error },
|
||||
'should reject message from different user'
|
||||
);
|
||||
}
|
||||
|
||||
// should reject chat from different user
|
||||
{
|
||||
const messageId = await createCopilotMessage(app, token, sessionId);
|
||||
{
|
||||
const { token } = await signUp(app, 'a2', 'a2@affine.pro', '123456');
|
||||
await t.throwsAsync(
|
||||
chatWithText(app, token.token, sessionId, messageId),
|
||||
{ instanceOf: Error },
|
||||
'should reject chat from different user'
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== history ====================
|
||||
|
||||
test('should be able to list history', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const { id: workspaceId } = await createWorkspace(app, token);
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
token,
|
||||
workspaceId,
|
||||
randomUUID(),
|
||||
promptName
|
||||
);
|
||||
|
||||
const messageId = await createCopilotMessage(app, token, sessionId);
|
||||
await chatWithText(app, token, sessionId, messageId);
|
||||
|
||||
const histories = await getHistories(app, token, { workspaceId });
|
||||
t.deepEqual(
|
||||
histories.map(h => h.messages.map(m => m.content)),
|
||||
[['generate text to text']],
|
||||
'should be able to list history'
|
||||
);
|
||||
});
|
||||
|
||||
test('should reject request that user have not permission', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const {
|
||||
token: { token: anotherToken },
|
||||
} = await signUp(app, 'a1', 'a1@affine.pro', '123456');
|
||||
const { id: workspaceId } = await createWorkspace(app, anotherToken);
|
||||
|
||||
// should reject request that user have not permission
|
||||
{
|
||||
await t.throwsAsync(
|
||||
getHistories(app, token, { workspaceId }),
|
||||
{ instanceOf: Error },
|
||||
'should reject request that user have not permission'
|
||||
);
|
||||
}
|
||||
|
||||
// should able to list history after user have permission
|
||||
{
|
||||
const inviteId = await inviteUser(
|
||||
app,
|
||||
anotherToken,
|
||||
workspaceId,
|
||||
'darksky@affine.pro',
|
||||
'Admin'
|
||||
);
|
||||
await acceptInviteById(app, workspaceId, inviteId, false);
|
||||
|
||||
t.deepEqual(
|
||||
await getHistories(app, token, { workspaceId }),
|
||||
[],
|
||||
'should able to list history after user have permission'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
anotherToken,
|
||||
workspaceId,
|
||||
randomUUID(),
|
||||
promptName
|
||||
);
|
||||
|
||||
const messageId = await createCopilotMessage(app, anotherToken, sessionId);
|
||||
await chatWithText(app, anotherToken, sessionId, messageId);
|
||||
|
||||
const histories = await getHistories(app, anotherToken, { workspaceId });
|
||||
t.deepEqual(
|
||||
histories.map(h => h.messages.map(m => m.content)),
|
||||
[['generate text to text']],
|
||||
'should able to list history'
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
await getHistories(app, token, { workspaceId }),
|
||||
[],
|
||||
'should not list history created by another user'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -5,17 +5,28 @@ import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
|
||||
import { AuthService } from '../src/core/auth';
|
||||
import { QuotaManagementService, QuotaModule } from '../src/core/quota';
|
||||
import { QuotaModule } from '../src/core/quota';
|
||||
import { ConfigModule } from '../src/fundamentals/config';
|
||||
import { CopilotModule } from '../src/plugins/copilot';
|
||||
import { PromptService } from '../src/plugins/copilot/prompt';
|
||||
import {
|
||||
CopilotProviderService,
|
||||
registerCopilotProvider,
|
||||
} from '../src/plugins/copilot/providers';
|
||||
import { ChatSessionService } from '../src/plugins/copilot/session';
|
||||
import {
|
||||
CopilotCapability,
|
||||
CopilotProviderType,
|
||||
} from '../src/plugins/copilot/types';
|
||||
import { createTestingModule } from './utils';
|
||||
import { MockCopilotTestProvider } from './utils/copilot';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
auth: AuthService;
|
||||
quotaManager: QuotaManagementService;
|
||||
module: TestingModule;
|
||||
prompt: PromptService;
|
||||
provider: CopilotProviderService;
|
||||
session: ChatSessionService;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
@@ -27,6 +38,9 @@ test.beforeEach(async t => {
|
||||
openai: {
|
||||
apiKey: '1',
|
||||
},
|
||||
fal: {
|
||||
apiKey: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -35,26 +49,37 @@ test.beforeEach(async t => {
|
||||
],
|
||||
});
|
||||
|
||||
const quotaManager = module.get(QuotaManagementService);
|
||||
const auth = module.get(AuthService);
|
||||
const prompt = module.get(PromptService);
|
||||
const provider = module.get(CopilotProviderService);
|
||||
const session = module.get(ChatSessionService);
|
||||
|
||||
t.context.module = module;
|
||||
t.context.quotaManager = quotaManager;
|
||||
t.context.auth = auth;
|
||||
t.context.prompt = prompt;
|
||||
t.context.provider = provider;
|
||||
t.context.session = session;
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
await t.context.module.close();
|
||||
});
|
||||
|
||||
let userId: string;
|
||||
test.beforeEach(async t => {
|
||||
const { auth } = t.context;
|
||||
const user = await auth.signUp('test', 'darksky@affine.pro', '123456');
|
||||
userId = user.id;
|
||||
});
|
||||
|
||||
// ==================== prompt ====================
|
||||
|
||||
test('should be able to manage prompt', async t => {
|
||||
const { prompt } = t.context;
|
||||
|
||||
t.is((await prompt.list()).length, 0, 'should have no prompt');
|
||||
|
||||
await prompt.set('test', [
|
||||
await prompt.set('test', 'test', [
|
||||
{ role: 'system', content: 'hello' },
|
||||
{ role: 'user', content: 'hello' },
|
||||
]);
|
||||
@@ -91,7 +116,7 @@ test('should be able to render prompt', async t => {
|
||||
content: 'hello world',
|
||||
};
|
||||
|
||||
await prompt.set('test', [msg]);
|
||||
await prompt.set('test', 'test', [msg]);
|
||||
const testPrompt = await prompt.get('test');
|
||||
t.assert(testPrompt, 'should have prompt');
|
||||
t.is(
|
||||
@@ -126,7 +151,7 @@ test('should be able to render listed prompt', async t => {
|
||||
links: ['https://affine.pro', 'https://github.com/toeverything/affine'],
|
||||
};
|
||||
|
||||
await prompt.set('test', [msg]);
|
||||
await prompt.set('test', 'test', [msg]);
|
||||
const testPrompt = await prompt.get('test');
|
||||
|
||||
t.is(
|
||||
@@ -135,3 +160,265 @@ test('should be able to render listed prompt', async t => {
|
||||
'should render the prompt'
|
||||
);
|
||||
});
|
||||
|
||||
// ==================== session ====================
|
||||
|
||||
test('should be able to manage chat session', async t => {
|
||||
const { prompt, session } = t.context;
|
||||
|
||||
await prompt.set('prompt', 'model', [
|
||||
{ role: 'system', content: 'hello {{word}}' },
|
||||
]);
|
||||
|
||||
const sessionId = await session.create({
|
||||
docId: 'test',
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
});
|
||||
t.truthy(sessionId, 'should create session');
|
||||
|
||||
const s = (await session.get(sessionId))!;
|
||||
t.is(s.config.sessionId, sessionId, 'should get session');
|
||||
t.is(s.config.promptName, 'prompt', 'should have prompt name');
|
||||
t.is(s.model, 'model', 'should have model');
|
||||
|
||||
const params = { word: 'world' };
|
||||
|
||||
s.push({ role: 'user', content: 'hello', createdAt: new Date() });
|
||||
// @ts-expect-error
|
||||
const finalMessages = s.finish(params).map(({ createdAt: _, ...m }) => m);
|
||||
t.deepEqual(
|
||||
finalMessages,
|
||||
[
|
||||
{ content: 'hello world', params, role: 'system' },
|
||||
{ content: 'hello', role: 'user' },
|
||||
],
|
||||
'should generate the final message'
|
||||
);
|
||||
await s.save();
|
||||
|
||||
const s1 = (await session.get(sessionId))!;
|
||||
t.deepEqual(
|
||||
// @ts-expect-error
|
||||
s1.finish(params).map(({ createdAt: _, ...m }) => m),
|
||||
finalMessages,
|
||||
'should same as before message'
|
||||
);
|
||||
t.deepEqual(
|
||||
// @ts-expect-error
|
||||
s1.finish({}).map(({ createdAt: _, ...m }) => m),
|
||||
[
|
||||
{ content: 'hello ', params: {}, role: 'system' },
|
||||
{ content: 'hello', role: 'user' },
|
||||
],
|
||||
'should generate different message with another params'
|
||||
);
|
||||
});
|
||||
|
||||
test('should be able to process message id', async t => {
|
||||
const { prompt, session } = t.context;
|
||||
|
||||
await prompt.set('prompt', 'model', [
|
||||
{ role: 'system', content: 'hello {{word}}' },
|
||||
]);
|
||||
|
||||
const sessionId = await session.create({
|
||||
docId: 'test',
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
});
|
||||
const s = (await session.get(sessionId))!;
|
||||
|
||||
const textMessage = (await session.createMessage({
|
||||
sessionId,
|
||||
content: 'hello',
|
||||
}))!;
|
||||
const anotherSessionMessage = (await session.createMessage({
|
||||
sessionId: 'another-session-id',
|
||||
}))!;
|
||||
|
||||
await t.notThrowsAsync(
|
||||
s.pushByMessageId(textMessage),
|
||||
'should push by message id'
|
||||
);
|
||||
await t.throwsAsync(
|
||||
s.pushByMessageId(anotherSessionMessage),
|
||||
{
|
||||
instanceOf: Error,
|
||||
},
|
||||
'should throw error if push by another session message id'
|
||||
);
|
||||
await t.throwsAsync(
|
||||
s.pushByMessageId('invalid'),
|
||||
{ instanceOf: Error },
|
||||
'should throw error if push by invalid message id'
|
||||
);
|
||||
});
|
||||
|
||||
test('should be able to generate with message id', async t => {
|
||||
const { prompt, session } = t.context;
|
||||
|
||||
await prompt.set('prompt', 'model', [
|
||||
{ role: 'system', content: 'hello {{word}}' },
|
||||
]);
|
||||
|
||||
// text message
|
||||
{
|
||||
const sessionId = await session.create({
|
||||
docId: 'test',
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
});
|
||||
const s = (await session.get(sessionId))!;
|
||||
|
||||
const message = (await session.createMessage({
|
||||
sessionId,
|
||||
content: 'hello',
|
||||
}))!;
|
||||
|
||||
await s.pushByMessageId(message);
|
||||
const finalMessages = s
|
||||
.finish({ word: 'world' })
|
||||
.map(({ content }) => content);
|
||||
t.deepEqual(finalMessages, ['hello world', 'hello']);
|
||||
}
|
||||
|
||||
// attachment message
|
||||
{
|
||||
const sessionId = await session.create({
|
||||
docId: 'test',
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
});
|
||||
const s = (await session.get(sessionId))!;
|
||||
|
||||
const message = (await session.createMessage({
|
||||
sessionId,
|
||||
attachments: ['https://affine.pro/example.jpg'],
|
||||
}))!;
|
||||
|
||||
await s.pushByMessageId(message);
|
||||
const finalMessages = s
|
||||
.finish({ word: 'world' })
|
||||
.map(({ attachments }) => attachments);
|
||||
t.deepEqual(finalMessages, [
|
||||
// system prompt
|
||||
undefined,
|
||||
// user prompt
|
||||
['https://affine.pro/example.jpg'],
|
||||
]);
|
||||
}
|
||||
|
||||
// empty message
|
||||
{
|
||||
const sessionId = await session.create({
|
||||
docId: 'test',
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
});
|
||||
const s = (await session.get(sessionId))!;
|
||||
|
||||
const message = (await session.createMessage({
|
||||
sessionId,
|
||||
}))!;
|
||||
|
||||
await s.pushByMessageId(message);
|
||||
const finalMessages = s
|
||||
.finish({ word: 'world' })
|
||||
.map(({ content }) => content);
|
||||
// empty message should be filtered
|
||||
t.deepEqual(finalMessages, ['hello world']);
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== provider ====================
|
||||
|
||||
test('should be able to get provider', async t => {
|
||||
const { provider } = t.context;
|
||||
|
||||
{
|
||||
const p = provider.getProviderByCapability(CopilotCapability.TextToText);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'openai',
|
||||
'should get provider support text-to-text'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const p = provider.getProviderByCapability(
|
||||
CopilotCapability.TextToEmbedding
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'openai',
|
||||
'should get provider support text-to-embedding'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const p = provider.getProviderByCapability(CopilotCapability.TextToImage);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'fal',
|
||||
'should get provider support text-to-image'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const p = provider.getProviderByCapability(CopilotCapability.ImageToImage);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'fal',
|
||||
'should get provider support image-to-image'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const p = provider.getProviderByCapability(CopilotCapability.ImageToText);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'openai',
|
||||
'should get provider support image-to-text'
|
||||
);
|
||||
}
|
||||
|
||||
// text-to-image use fal by default, but this case can use
|
||||
// model dall-e-3 to select openai provider
|
||||
{
|
||||
const p = provider.getProviderByCapability(
|
||||
CopilotCapability.TextToImage,
|
||||
'dall-e-3'
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'openai',
|
||||
'should get provider support text-to-image and model'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should be able to register test provider', async t => {
|
||||
const { provider } = t.context;
|
||||
registerCopilotProvider(MockCopilotTestProvider);
|
||||
|
||||
const assertProvider = (cap: CopilotCapability) => {
|
||||
const p = provider.getProviderByCapability(cap, 'test');
|
||||
t.is(
|
||||
p?.type,
|
||||
CopilotProviderType.Test,
|
||||
`should get test provider with ${cap}`
|
||||
);
|
||||
};
|
||||
|
||||
assertProvider(CopilotCapability.TextToText);
|
||||
assertProvider(CopilotCapability.TextToEmbedding);
|
||||
assertProvider(CopilotCapability.TextToImage);
|
||||
assertProvider(CopilotCapability.ImageToImage);
|
||||
assertProvider(CopilotCapability.ImageToText);
|
||||
});
|
||||
|
||||
305
packages/backend/server/tests/utils/copilot.ts
Normal file
305
packages/backend/server/tests/utils/copilot.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
|
||||
import {
|
||||
DEFAULT_DIMENSIONS,
|
||||
OpenAIProvider,
|
||||
} from '../../src/plugins/copilot/providers/openai';
|
||||
import {
|
||||
CopilotCapability,
|
||||
CopilotImageToImageProvider,
|
||||
CopilotImageToTextProvider,
|
||||
CopilotProviderType,
|
||||
CopilotTextToEmbeddingProvider,
|
||||
CopilotTextToImageProvider,
|
||||
CopilotTextToTextProvider,
|
||||
PromptMessage,
|
||||
} from '../../src/plugins/copilot/types';
|
||||
import { gql } from './common';
|
||||
import { handleGraphQLError } from './utils';
|
||||
|
||||
export class MockCopilotTestProvider
|
||||
extends OpenAIProvider
|
||||
implements
|
||||
CopilotTextToTextProvider,
|
||||
CopilotTextToEmbeddingProvider,
|
||||
CopilotTextToImageProvider,
|
||||
CopilotImageToImageProvider,
|
||||
CopilotImageToTextProvider
|
||||
{
|
||||
override readonly availableModels = ['test'];
|
||||
static override readonly capabilities = [
|
||||
CopilotCapability.TextToText,
|
||||
CopilotCapability.TextToEmbedding,
|
||||
CopilotCapability.TextToImage,
|
||||
CopilotCapability.ImageToImage,
|
||||
CopilotCapability.ImageToText,
|
||||
];
|
||||
|
||||
override get type(): CopilotProviderType {
|
||||
return CopilotProviderType.Test;
|
||||
}
|
||||
|
||||
override getCapabilities(): CopilotCapability[] {
|
||||
return MockCopilotTestProvider.capabilities;
|
||||
}
|
||||
|
||||
override isModelAvailable(model: string): boolean {
|
||||
return this.availableModels.includes(model);
|
||||
}
|
||||
|
||||
// ====== text to text ======
|
||||
|
||||
override async generateText(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'test',
|
||||
_options: {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
this.checkParams({ messages, model });
|
||||
return 'generate text to text';
|
||||
}
|
||||
|
||||
override async *generateTextStream(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'gpt-3.5-turbo',
|
||||
options: {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
): AsyncIterable<string> {
|
||||
this.checkParams({ messages, model });
|
||||
|
||||
const result = 'generate text to text stream';
|
||||
for await (const message of result) {
|
||||
yield message;
|
||||
if (options.signal?.aborted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====== text to embedding ======
|
||||
|
||||
override async generateEmbedding(
|
||||
messages: string | string[],
|
||||
model: string,
|
||||
options: {
|
||||
dimensions: number;
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = { dimensions: DEFAULT_DIMENSIONS }
|
||||
): Promise<number[][]> {
|
||||
messages = Array.isArray(messages) ? messages : [messages];
|
||||
this.checkParams({ embeddings: messages, model });
|
||||
|
||||
return [Array.from(randomBytes(options.dimensions)).map(v => v % 128)];
|
||||
}
|
||||
|
||||
// ====== text to image ======
|
||||
override async generateImages(
|
||||
messages: PromptMessage[],
|
||||
_model: string = 'test',
|
||||
_options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
): Promise<Array<string>> {
|
||||
const { content: prompt } = messages.pop() || {};
|
||||
if (!prompt) {
|
||||
throw new Error('Prompt is required');
|
||||
}
|
||||
|
||||
return ['https://example.com/image.jpg'];
|
||||
}
|
||||
|
||||
override async *generateImagesStream(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'dall-e-3',
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
): AsyncIterable<string> {
|
||||
const ret = await this.generateImages(messages, model, options);
|
||||
for (const url of ret) {
|
||||
yield url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCopilotSession(
|
||||
app: INestApplication,
|
||||
userToken: string,
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
promptName: string
|
||||
): Promise<string> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(userToken, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation createCopilotSession($options: CreateChatSessionInput!) {
|
||||
createCopilotSession(options: $options)
|
||||
}
|
||||
`,
|
||||
variables: { options: { workspaceId, docId, promptName } },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
handleGraphQLError(res);
|
||||
|
||||
return res.body.data.createCopilotSession;
|
||||
}
|
||||
|
||||
export async function createCopilotMessage(
|
||||
app: INestApplication,
|
||||
userToken: string,
|
||||
sessionId: string,
|
||||
content?: string,
|
||||
attachments?: string[],
|
||||
blobs?: ArrayBuffer[],
|
||||
params?: Record<string, string>
|
||||
): Promise<string> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(userToken, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation createCopilotMessage($options: CreateChatMessageInput!) {
|
||||
createCopilotMessage(options: $options)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
options: { sessionId, content, attachments, blobs, params },
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
handleGraphQLError(res);
|
||||
|
||||
return res.body.data.createCopilotMessage;
|
||||
}
|
||||
|
||||
export async function chatWithText(
|
||||
app: INestApplication,
|
||||
userToken: string,
|
||||
sessionId: string,
|
||||
messageId: string,
|
||||
prefix = ''
|
||||
): Promise<string> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.get(`/api/copilot/chat/${sessionId}${prefix}?messageId=${messageId}`)
|
||||
.auth(userToken, { type: 'bearer' })
|
||||
.expect(200);
|
||||
|
||||
return res.text;
|
||||
}
|
||||
|
||||
export async function chatWithTextStream(
|
||||
app: INestApplication,
|
||||
userToken: string,
|
||||
sessionId: string,
|
||||
messageId: string
|
||||
) {
|
||||
return chatWithText(app, userToken, sessionId, messageId, '/stream');
|
||||
}
|
||||
|
||||
export async function chatWithImages(
|
||||
app: INestApplication,
|
||||
userToken: string,
|
||||
sessionId: string,
|
||||
messageId: string
|
||||
) {
|
||||
return chatWithText(app, userToken, sessionId, messageId, '/images');
|
||||
}
|
||||
|
||||
export function textToEventStream(
|
||||
content: string | string[],
|
||||
id: string,
|
||||
event = 'message'
|
||||
): string {
|
||||
return (
|
||||
Array.from(content)
|
||||
.map(x => `\nevent: ${event}\nid: ${id}\ndata: ${x}`)
|
||||
.join('\n') + '\n\n'
|
||||
);
|
||||
}
|
||||
|
||||
type ChatMessage = {
|
||||
role: string;
|
||||
content: string;
|
||||
attachments: string[] | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type History = {
|
||||
sessionId: string;
|
||||
tokens: number;
|
||||
action: string | null;
|
||||
createdAt: string;
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
|
||||
export async function getHistories(
|
||||
app: INestApplication,
|
||||
userToken: string,
|
||||
variables: {
|
||||
workspaceId: string;
|
||||
docId?: string;
|
||||
options?: {
|
||||
sessionId?: string;
|
||||
action?: boolean;
|
||||
limit?: number;
|
||||
skip?: number;
|
||||
};
|
||||
}
|
||||
): Promise<History[]> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(userToken, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
query getCopilotHistories(
|
||||
$workspaceId: String!
|
||||
$docId: String
|
||||
$options: QueryChatHistoriesInput
|
||||
) {
|
||||
currentUser {
|
||||
copilot(workspaceId: $workspaceId) {
|
||||
histories(docId: $docId, options: $options) {
|
||||
sessionId
|
||||
tokens
|
||||
action
|
||||
createdAt
|
||||
messages {
|
||||
role
|
||||
content
|
||||
attachments
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
handleGraphQLError(res);
|
||||
|
||||
return res.body.data.currentUser?.copilot?.histories || [];
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Test, TestingModuleBuilder } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import type { Response } from 'supertest';
|
||||
|
||||
import { AppModule, FunctionalityModules } from '../../src/app.module';
|
||||
import { AuthGuard, AuthModule } from '../../src/core/auth';
|
||||
@@ -136,3 +137,12 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
|
||||
app,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleGraphQLError(resp: Response) {
|
||||
const { errors } = resp.body;
|
||||
if (errors) {
|
||||
const cause = errors[0];
|
||||
const stacktrace = cause.extensions?.stacktrace;
|
||||
throw new Error(stacktrace ? stacktrace.join('\n') : cause.message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "../storage/tsconfig.json"
|
||||
"path": "../native/tsconfig.json"
|
||||
}
|
||||
],
|
||||
"ts-node": {
|
||||
|
||||
4
packages/common/env/package.json
vendored
4
packages/common/env/package.json
vendored
@@ -3,8 +3,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@blocksuite/global": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/store": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/global": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/store": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"vitest": "1.4.0"
|
||||
|
||||
1
packages/common/env/src/global.ts
vendored
1
packages/common/env/src/global.ts
vendored
@@ -26,7 +26,6 @@ export const runtimeFlagsSchema = z.object({
|
||||
allowLocalWorkspace: z.boolean(),
|
||||
// this is for the electron app
|
||||
serverUrlPrefix: z.string(),
|
||||
enableMoveDatabase: z.boolean(),
|
||||
appVersion: z.string(),
|
||||
editorVersion: z.string(),
|
||||
appBuildType: z.union([
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/global": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/store": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/global": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/store": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@datastructures-js/binary-search-tree": "^5.3.2",
|
||||
"foxact": "^0.2.33",
|
||||
"jotai": "^2.8.0",
|
||||
@@ -28,8 +28,8 @@
|
||||
"devDependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@blocksuite/block-std": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/presets": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/block-std": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/presets": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"async-call-rpc": "^6.4.0",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { catchError, EMPTY, mergeMap, switchMap } from 'rxjs';
|
||||
import { catchError, EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
||||
|
||||
import { Entity } from '../../../framework';
|
||||
import {
|
||||
@@ -59,7 +59,7 @@ export class WorkspaceProfile extends Entity<{ metadata: WorkspaceMetadata }> {
|
||||
}
|
||||
|
||||
revalidate = effect(
|
||||
switchMap(() => {
|
||||
exhaustMap(() => {
|
||||
const provider = this.provider;
|
||||
if (!provider) {
|
||||
return EMPTY;
|
||||
|
||||
@@ -32,14 +32,14 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocksuite/global": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/global": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"idb": "^8.0.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"y-provider": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/blocks": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/store": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/store": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-dts": "3.8.1",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/store": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/store": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-dts": "3.7.3",
|
||||
"vitest": "1.4.0",
|
||||
|
||||
@@ -75,12 +75,12 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/block-std": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/global": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/block-std": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/global": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/icons": "2.1.46",
|
||||
"@blocksuite/presets": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/store": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/presets": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/store": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@storybook/addon-actions": "^7.6.17",
|
||||
"@storybook/addon-essentials": "^7.6.17",
|
||||
"@storybook/addon-interactions": "^7.6.17",
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
"@affine/graphql": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@blocksuite/block-std": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/global": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/block-std": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/global": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/icons": "2.1.46",
|
||||
"@blocksuite/inline": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/presets": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/store": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/inline": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/presets": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/store": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -26,27 +26,27 @@ type Translate = ReturnType<typeof useAFFiNEI18N>;
|
||||
|
||||
const getPlayList = (t: Translate): Array<PlayListItem> => [
|
||||
{
|
||||
video: '/onboarding/ai-onboarding.general.1.mov',
|
||||
video: '/onboarding/ai-onboarding.general.1.mp4',
|
||||
title: t['com.affine.ai-onboarding.general.1.title'](),
|
||||
desc: t['com.affine.ai-onboarding.general.1.description'](),
|
||||
},
|
||||
{
|
||||
video: '/onboarding/ai-onboarding.general.2.mov',
|
||||
video: '/onboarding/ai-onboarding.general.2.mp4',
|
||||
title: t['com.affine.ai-onboarding.general.2.title'](),
|
||||
desc: t['com.affine.ai-onboarding.general.2.description'](),
|
||||
},
|
||||
{
|
||||
video: '/onboarding/ai-onboarding.general.3.mov',
|
||||
video: '/onboarding/ai-onboarding.general.3.mp4',
|
||||
title: t['com.affine.ai-onboarding.general.3.title'](),
|
||||
desc: t['com.affine.ai-onboarding.general.3.description'](),
|
||||
},
|
||||
{
|
||||
video: '/onboarding/ai-onboarding.general.4.mov',
|
||||
video: '/onboarding/ai-onboarding.general.4.mp4',
|
||||
title: t['com.affine.ai-onboarding.general.4.title'](),
|
||||
desc: t['com.affine.ai-onboarding.general.4.description'](),
|
||||
},
|
||||
{
|
||||
video: '/onboarding/ai-onboarding.general.1.mov',
|
||||
video: '/onboarding/ai-onboarding.general.5.mp4',
|
||||
title: t['com.affine.ai-onboarding.general.5.title'](),
|
||||
desc: (
|
||||
<Trans
|
||||
@@ -241,17 +241,17 @@ export const AIOnboardingGeneral = ({
|
||||
<Button
|
||||
className={styles.baseActionButton}
|
||||
size="large"
|
||||
onClick={closeAndDismiss}
|
||||
onClick={goToPricingPlans}
|
||||
>
|
||||
{t['com.affine.ai-onboarding.general.try-for-free']()}
|
||||
{t['com.affine.ai-onboarding.general.purchase']()}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.baseActionButton}
|
||||
size="large"
|
||||
onClick={goToPricingPlans}
|
||||
onClick={closeAndDismiss}
|
||||
type="primary"
|
||||
>
|
||||
{t['com.affine.ai-onboarding.general.purchase']()}
|
||||
{t['com.affine.ai-onboarding.general.try-for-free']()}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -64,7 +64,7 @@ export const AfterSignInSendEmail = ({
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({
|
||||
message: 'Failed to send email, please try again.',
|
||||
title: 'Failed to send email, please try again.',
|
||||
});
|
||||
}
|
||||
setIsSending(false);
|
||||
|
||||
@@ -64,7 +64,7 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({
|
||||
message: 'Failed to send email, please try again.',
|
||||
title: 'Failed to send email, please try again.',
|
||||
});
|
||||
}
|
||||
setIsSending(false);
|
||||
|
||||
@@ -67,7 +67,7 @@ function OAuthProvider({
|
||||
await authService.signInOauth(provider, redirectUri);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({ message: 'Failed to sign in, please try again.' });
|
||||
notify.error({ title: 'Failed to sign in, please try again.' });
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
mixpanel.track('OAuth', { provider });
|
||||
|
||||
@@ -60,7 +60,7 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({
|
||||
message: 'Failed to send email, please try again.',
|
||||
title: 'Failed to send email, please try again.',
|
||||
});
|
||||
// TODO: handle error better
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
|
||||
// TODO: better error handling
|
||||
notify.error({
|
||||
message: 'Failed to send email. Please try again.',
|
||||
title: 'Failed to send email. Please try again.',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import { ExportPanel } from './export';
|
||||
import { LabelsPanel } from './labels';
|
||||
import { MembersPanel } from './members';
|
||||
import { ProfilePanel } from './profile';
|
||||
import { StoragePanel } from './storage';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
export const WorkspaceSettingDetail = ({
|
||||
@@ -70,9 +69,6 @@ export const WorkspaceSettingDetail = ({
|
||||
</SettingWrapper>
|
||||
{environment.isDesktop && (
|
||||
<SettingWrapper title={t['Storage and Export']()}>
|
||||
{runtimeConfig.enableMoveDatabase ? (
|
||||
<StoragePanel workspaceMetadata={workspaceMetadata} />
|
||||
) : null}
|
||||
<ExportPanel
|
||||
workspace={workspace}
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
|
||||
@@ -155,6 +155,7 @@ export const ProfilePanel = () => {
|
||||
name={name}
|
||||
imageProps={avatarImageProps}
|
||||
fallbackProps={avatarImageProps}
|
||||
hoverWrapperProps={avatarImageProps}
|
||||
colorfulFallback
|
||||
hoverIcon={isOwner ? <CameraIcon /> : undefined}
|
||||
onRemove={canAdjustAvatar ? handleRemoveUserAvatar : undefined}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import { FlexWrapper, toast } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
const useDBFileSecondaryPath = (workspaceId: string) => {
|
||||
const [path, setPath] = useState<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (apis && events && environment.isDesktop) {
|
||||
apis?.workspace
|
||||
.getMeta(workspaceId)
|
||||
.then(meta => {
|
||||
setPath(meta.secondaryDBPath);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
return events.workspace.onMetaChange((newMeta: any) => {
|
||||
if (newMeta.workspaceId === workspaceId) {
|
||||
const meta = newMeta.meta;
|
||||
setPath(meta.secondaryDBPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}, [workspaceId]);
|
||||
return path;
|
||||
};
|
||||
|
||||
interface StoragePanelProps {
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
}
|
||||
|
||||
export const StoragePanel = ({ workspaceMetadata }: StoragePanelProps) => {
|
||||
const workspaceId = workspaceMetadata.id;
|
||||
const t = useAFFiNEI18N();
|
||||
const secondaryPath = useDBFileSecondaryPath(workspaceId);
|
||||
|
||||
const [moveToInProgress, setMoveToInProgress] = useState<boolean>(false);
|
||||
const onRevealDBFile = useCallback(() => {
|
||||
apis?.dialog.revealDBFile(workspaceId).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [workspaceId]);
|
||||
|
||||
const handleMoveTo = useCallback(() => {
|
||||
if (moveToInProgress) {
|
||||
return;
|
||||
}
|
||||
setMoveToInProgress(true);
|
||||
apis?.dialog
|
||||
.moveDBFile(workspaceId)
|
||||
.then(result => {
|
||||
if (!result?.error && !result?.canceled) {
|
||||
toast(t['Move folder success']());
|
||||
} else if (result?.error) {
|
||||
toast(t[result.error]());
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast(t['UNKNOWN_ERROR']());
|
||||
})
|
||||
.finally(() => {
|
||||
setMoveToInProgress(false);
|
||||
});
|
||||
}, [moveToInProgress, t, workspaceId]);
|
||||
|
||||
const rowContent = useMemo(
|
||||
() =>
|
||||
secondaryPath ? (
|
||||
<FlexWrapper justifyContent="space-between">
|
||||
<Tooltip
|
||||
content={t['com.affine.settings.storage.db-location.change-hint']()}
|
||||
side="top"
|
||||
align="start"
|
||||
>
|
||||
<Button
|
||||
data-testid="move-folder"
|
||||
// className={style.urlButton}
|
||||
size="large"
|
||||
onClick={handleMoveTo}
|
||||
>
|
||||
{secondaryPath}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
data-testid="reveal-folder"
|
||||
data-disabled={moveToInProgress}
|
||||
onClick={onRevealDBFile}
|
||||
>
|
||||
{t['Open folder']()}
|
||||
</Button>
|
||||
</FlexWrapper>
|
||||
) : (
|
||||
<Button
|
||||
data-testid="move-folder"
|
||||
data-disabled={moveToInProgress}
|
||||
onClick={handleMoveTo}
|
||||
>
|
||||
{t['Move folder']()}
|
||||
</Button>
|
||||
),
|
||||
[handleMoveTo, moveToInProgress, onRevealDBFile, secondaryPath, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['Storage']()}
|
||||
desc={t[
|
||||
secondaryPath
|
||||
? 'com.affine.settings.storage.description-alt'
|
||||
: 'com.affine.settings.storage.description'
|
||||
]()}
|
||||
spreadCol={!secondaryPath}
|
||||
>
|
||||
{rowContent}
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
@@ -158,9 +158,14 @@ export class CopilotClient {
|
||||
}
|
||||
|
||||
// Text or image to images
|
||||
imagesStream(messageId: string, sessionId: string) {
|
||||
return new EventSource(
|
||||
`${this.backendUrl}/api/copilot/chat/${sessionId}/images?messageId=${messageId}`
|
||||
imagesStream(messageId: string, sessionId: string, seed?: string) {
|
||||
const url = new URL(
|
||||
`${this.backendUrl}/api/copilot/chat/${sessionId}/images`
|
||||
);
|
||||
url.searchParams.set('messageId', messageId);
|
||||
if (seed) {
|
||||
url.searchParams.set('seed', seed);
|
||||
}
|
||||
return new EventSource(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ const provideAction = <T extends AIAction>(
|
||||
if (TRACKED_ACTIONS[id]) {
|
||||
const wrappedFn: typeof action = (opts, ...rest) => {
|
||||
mixpanel.track('AI', {
|
||||
resolve: action,
|
||||
resolve: id,
|
||||
docId: opts.docId,
|
||||
workspaceId: opts.workspaceId,
|
||||
});
|
||||
@@ -278,10 +278,10 @@ export function setupAIProvider() {
|
||||
});
|
||||
|
||||
provideAction('makeItReal', options => {
|
||||
return textToText({
|
||||
return toImage({
|
||||
...options,
|
||||
promptName: 'Make it real',
|
||||
params: options.params,
|
||||
seed: options.seed,
|
||||
content:
|
||||
options.content ||
|
||||
'Here are the latest wireframes. Could you make a new website based on these wireframes and notes and send back just the html file?',
|
||||
|
||||
@@ -31,6 +31,10 @@ export type TextToTextOptions = {
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
export type ToImageOptions = TextToTextOptions & {
|
||||
seed?: string;
|
||||
};
|
||||
|
||||
export function createChatSession({
|
||||
workspaceId,
|
||||
docId,
|
||||
@@ -175,8 +179,9 @@ export function toImage({
|
||||
content,
|
||||
attachments,
|
||||
params,
|
||||
seed,
|
||||
timeout = TIMEOUT,
|
||||
}: TextToTextOptions) {
|
||||
}: ToImageOptions) {
|
||||
return {
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
const { messageId, sessionId } = await createSessionMessage({
|
||||
@@ -188,7 +193,7 @@ export function toImage({
|
||||
params,
|
||||
});
|
||||
|
||||
const eventSource = client.imagesStream(messageId, sessionId);
|
||||
const eventSource = client.imagesStream(messageId, sessionId, seed);
|
||||
for await (const event of toTextStream(eventSource, { timeout })) {
|
||||
if (event.type === 'attachment') {
|
||||
yield event.data;
|
||||
|
||||
@@ -233,9 +233,8 @@ export const SignOutConfirmModal = () => {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// TODO: i18n
|
||||
notify({
|
||||
style: 'alert',
|
||||
message: 'Failed to sign out',
|
||||
notify.error({
|
||||
title: 'Failed to sign out',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ export function Telemetry() {
|
||||
track_pageview: true,
|
||||
persistence: 'localStorage',
|
||||
});
|
||||
}
|
||||
if (settings.enableTelemetry === false) {
|
||||
mixpanel.opt_out_tracking();
|
||||
if (settings.enableTelemetry === false) {
|
||||
mixpanel.opt_out_tracking();
|
||||
}
|
||||
}
|
||||
}, [settings.enableTelemetry]);
|
||||
return null;
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/native": "workspace:*",
|
||||
"@blocksuite/block-std": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/presets": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/store": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/block-std": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/presets": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@blocksuite/store": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@electron-forge/cli": "^7.3.0",
|
||||
"@electron-forge/core": "^7.3.0",
|
||||
"@electron-forge/core-utils": "^7.3.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.3.0",
|
||||
"@electron-forge/shared-types": "^7.3.0",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@pengx17/electron-forge-maker-appimage": "^1.2.0",
|
||||
"@pengx17/electron-forge-maker-appimage": "^1.2.1",
|
||||
"@sentry/electron": "^4.22.0",
|
||||
"@sentry/esbuild-plugin": "^2.16.1",
|
||||
"@sentry/react": "^7.109.0",
|
||||
|
||||
@@ -1,145 +1,38 @@
|
||||
import type { Subject } from 'rxjs';
|
||||
import {
|
||||
concat,
|
||||
defer,
|
||||
from,
|
||||
fromEvent,
|
||||
interval,
|
||||
lastValueFrom,
|
||||
merge,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
concatMap,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
ignoreElements,
|
||||
last,
|
||||
map,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { getWorkspaceMeta } from '../workspace/meta';
|
||||
import { workspaceSubjects } from '../workspace/subjects';
|
||||
import { SecondaryWorkspaceSQLiteDB } from './secondary-db';
|
||||
import type { WorkspaceSQLiteDB } from './workspace-db-adapter';
|
||||
import { openWorkspaceDatabase } from './workspace-db-adapter';
|
||||
|
||||
// export for testing
|
||||
export const db$Map = new Map<string, Observable<WorkspaceSQLiteDB>>();
|
||||
export const db$Map = new Map<string, Promise<WorkspaceSQLiteDB>>();
|
||||
|
||||
// use defer to prevent `app` is undefined while running tests
|
||||
const beforeQuit$ = defer(() => fromEvent(process, 'beforeExit'));
|
||||
|
||||
// return a stream that emit a single event when the subject completes
|
||||
function completed<T>(subject$: Subject<T>) {
|
||||
return new Observable(subscriber => {
|
||||
const sub = subject$.subscribe({
|
||||
complete: () => {
|
||||
subscriber.next();
|
||||
subscriber.complete();
|
||||
},
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
function getWorkspaceDB(id: string) {
|
||||
async function getWorkspaceDB(id: string) {
|
||||
let db = await db$Map.get(id);
|
||||
if (!db$Map.has(id)) {
|
||||
db$Map.set(
|
||||
id,
|
||||
from(openWorkspaceDatabase(id)).pipe(
|
||||
tap({
|
||||
next: db => {
|
||||
logger.info(
|
||||
'[ensureSQLiteDB] db connection established',
|
||||
db.workspaceId
|
||||
);
|
||||
},
|
||||
}),
|
||||
switchMap(db =>
|
||||
// takeUntil the polling stream, and then destroy the db
|
||||
concat(
|
||||
startPollingSecondaryDB(db).pipe(
|
||||
ignoreElements(),
|
||||
startWith(db),
|
||||
takeUntil(merge(beforeQuit$, completed(db.update$))),
|
||||
last(),
|
||||
tap({
|
||||
next() {
|
||||
logger.info(
|
||||
'[ensureSQLiteDB] polling secondary db complete',
|
||||
db.workspaceId
|
||||
);
|
||||
},
|
||||
})
|
||||
),
|
||||
defer(async () => {
|
||||
try {
|
||||
await db.destroy();
|
||||
db$Map.delete(id);
|
||||
return db;
|
||||
} catch (err) {
|
||||
logger.error('[ensureSQLiteDB] destroy db failed', err);
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
).pipe(startWith(db))
|
||||
),
|
||||
shareReplay(1)
|
||||
)
|
||||
);
|
||||
const promise = openWorkspaceDatabase(id);
|
||||
db$Map.set(id, promise);
|
||||
const _db = (db = await promise);
|
||||
const cleanup = () => {
|
||||
db$Map.delete(id);
|
||||
_db
|
||||
.destroy()
|
||||
.then(() => {
|
||||
logger.info('[ensureSQLiteDB] db connection closed', _db.workspaceId);
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error('[ensureSQLiteDB] destroy db failed', err);
|
||||
});
|
||||
};
|
||||
|
||||
db.update$.subscribe({
|
||||
complete: cleanup,
|
||||
});
|
||||
|
||||
process.on('beforeExit', cleanup);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return db$Map.get(id)!;
|
||||
}
|
||||
|
||||
function startPollingSecondaryDB(db: WorkspaceSQLiteDB) {
|
||||
return merge(
|
||||
getWorkspaceMeta(db.workspaceId),
|
||||
workspaceSubjects.meta$.pipe(
|
||||
map(({ meta }) => meta),
|
||||
filter(meta => meta.id === db.workspaceId)
|
||||
)
|
||||
).pipe(
|
||||
map(meta => meta?.secondaryDBPath),
|
||||
filter((p): p is string => !!p),
|
||||
distinctUntilChanged(),
|
||||
switchMap(path => {
|
||||
// on secondary db path change, destroy the old db and create a new one
|
||||
const secondaryDB = new SecondaryWorkspaceSQLiteDB(path, db);
|
||||
return new Observable<SecondaryWorkspaceSQLiteDB>(subscriber => {
|
||||
subscriber.next(secondaryDB);
|
||||
return () => {
|
||||
secondaryDB.destroy().catch(err => {
|
||||
subscriber.error(err);
|
||||
});
|
||||
};
|
||||
});
|
||||
}),
|
||||
switchMap(secondaryDB => {
|
||||
return interval(300000).pipe(
|
||||
startWith(0),
|
||||
concatMap(() => secondaryDB.pull()),
|
||||
tap({
|
||||
error: err => {
|
||||
logger.error(`[ensureSQLiteDB] polling secondary db error`, err);
|
||||
},
|
||||
complete: () => {
|
||||
logger.info('[ensureSQLiteDB] polling secondary db complete');
|
||||
},
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
return db!;
|
||||
}
|
||||
|
||||
export function ensureSQLiteDB(id: string) {
|
||||
return lastValueFrom(getWorkspaceDB(id).pipe(take(1)));
|
||||
return getWorkspaceDB(id);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { mainRPC } from '../main-rpc';
|
||||
import type { MainEventRegister } from '../type';
|
||||
import { ensureSQLiteDB } from './ensure-db';
|
||||
import { dbSubjects } from './subjects';
|
||||
|
||||
export * from './ensure-db';
|
||||
export * from './subjects';
|
||||
|
||||
export const dbHandlers = {
|
||||
getDocAsUpdates: async (workspaceId: string, subdocId?: string) => {
|
||||
@@ -17,7 +15,12 @@ export const dbHandlers = {
|
||||
subdocId?: string
|
||||
) => {
|
||||
const workspaceDB = await ensureSQLiteDB(workspaceId);
|
||||
return workspaceDB.applyUpdate(update, 'renderer', subdocId);
|
||||
return workspaceDB.addUpdateToSQLite([
|
||||
{
|
||||
data: update,
|
||||
docId: subdocId,
|
||||
},
|
||||
]);
|
||||
},
|
||||
addBlob: async (workspaceId: string, key: string, data: Uint8Array) => {
|
||||
const workspaceDB = await ensureSQLiteDB(workspaceId);
|
||||
@@ -40,17 +43,4 @@ export const dbHandlers = {
|
||||
},
|
||||
};
|
||||
|
||||
export const dbEvents = {
|
||||
onExternalUpdate: (
|
||||
fn: (update: {
|
||||
workspaceId: string;
|
||||
update: Uint8Array;
|
||||
docId?: string;
|
||||
}) => void
|
||||
) => {
|
||||
const sub = dbSubjects.externalUpdate$.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
} satisfies Record<string, MainEventRegister>;
|
||||
export const dbEvents = {} satisfies Record<string, MainEventRegister>;
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
import assert from 'node:assert';
|
||||
|
||||
import type { InsertRow } from '@affine/native';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import type { YOrigin } from '../type';
|
||||
import { getWorkspaceMeta } from '../workspace/meta';
|
||||
import { BaseSQLiteAdapter } from './base-db-adapter';
|
||||
import type { WorkspaceSQLiteDB } from './workspace-db-adapter';
|
||||
|
||||
const FLUSH_WAIT_TIME = 5000;
|
||||
const FLUSH_MAX_WAIT_TIME = 10000;
|
||||
|
||||
// todo: trim db when it is too big
|
||||
export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
role = 'secondary';
|
||||
yDoc = new YDoc();
|
||||
firstConnected = false;
|
||||
destroyed = false;
|
||||
|
||||
updateQueue: { data: Uint8Array; docId?: string }[] = [];
|
||||
|
||||
unsubscribers = new Set<() => void>();
|
||||
|
||||
constructor(
|
||||
public override path: string,
|
||||
public upstream: WorkspaceSQLiteDB
|
||||
) {
|
||||
super(path);
|
||||
this.init();
|
||||
logger.debug('[SecondaryWorkspaceSQLiteDB] created', this.workspaceId);
|
||||
}
|
||||
|
||||
getDoc(docId?: string) {
|
||||
if (!docId) {
|
||||
return this.yDoc;
|
||||
}
|
||||
// this should be pretty fast and we don't need to cache it
|
||||
for (const subdoc of this.yDoc.subdocs) {
|
||||
if (subdoc.guid === docId) {
|
||||
return subdoc;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
override async destroy() {
|
||||
await this.flushUpdateQueue();
|
||||
this.unsubscribers.forEach(unsub => unsub());
|
||||
this.yDoc.destroy();
|
||||
await super.destroy();
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
get workspaceId() {
|
||||
return this.upstream.workspaceId;
|
||||
}
|
||||
|
||||
// do not update db immediately, instead, push to a queue
|
||||
// and flush the queue in a future time
|
||||
async addUpdateToUpdateQueue(update: InsertRow) {
|
||||
this.updateQueue.push(update);
|
||||
await this.debouncedFlush();
|
||||
}
|
||||
|
||||
async flushUpdateQueue() {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
logger.debug(
|
||||
'flushUpdateQueue',
|
||||
this.workspaceId,
|
||||
'queue',
|
||||
this.updateQueue.length
|
||||
);
|
||||
const updates = [...this.updateQueue];
|
||||
this.updateQueue = [];
|
||||
await this.run(async () => {
|
||||
await this.addUpdateToSQLite(updates);
|
||||
});
|
||||
}
|
||||
|
||||
// flush after 5s, but will not wait for more than 10s
|
||||
debouncedFlush = debounce(this.flushUpdateQueue, FLUSH_WAIT_TIME, {
|
||||
maxWait: FLUSH_MAX_WAIT_TIME,
|
||||
});
|
||||
|
||||
runCounter = 0;
|
||||
|
||||
// wrap the fn with connect and close
|
||||
async run<T extends (...args: any[]) => any>(
|
||||
fn: T
|
||||
): Promise<
|
||||
(T extends (...args: any[]) => infer U ? Awaited<U> : unknown) | undefined
|
||||
> {
|
||||
try {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
await this.connectIfNeeded();
|
||||
this.runCounter++;
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
throw err;
|
||||
} finally {
|
||||
this.runCounter--;
|
||||
if (this.runCounter === 0) {
|
||||
// just close db, but not the yDoc
|
||||
await super.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupListener(docId?: string) {
|
||||
logger.debug(
|
||||
'SecondaryWorkspaceSQLiteDB:setupListener',
|
||||
this.workspaceId,
|
||||
docId
|
||||
);
|
||||
const doc = this.getDoc(docId);
|
||||
const upstreamDoc = this.upstream.getDoc(docId);
|
||||
if (!doc || !upstreamDoc) {
|
||||
logger.warn(
|
||||
'[SecondaryWorkspaceSQLiteDB] setupListener: doc not found',
|
||||
docId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const onUpstreamUpdate = (update: Uint8Array, origin: YOrigin) => {
|
||||
logger.debug(
|
||||
'SecondaryWorkspaceSQLiteDB:onUpstreamUpdate',
|
||||
origin,
|
||||
this.workspaceId,
|
||||
docId,
|
||||
update.length
|
||||
);
|
||||
if (origin === 'renderer' || origin === 'self') {
|
||||
// update to upstream yDoc should be replicated to self yDoc
|
||||
this.applyUpdate(update, 'upstream', docId);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelfUpdate = async (update: Uint8Array, origin: YOrigin) => {
|
||||
logger.debug(
|
||||
'SecondaryWorkspaceSQLiteDB:onSelfUpdate',
|
||||
origin,
|
||||
this.workspaceId,
|
||||
docId,
|
||||
update.length
|
||||
);
|
||||
// for self update from upstream, we need to push it to external DB
|
||||
if (origin === 'upstream') {
|
||||
await this.addUpdateToUpdateQueue({
|
||||
data: update,
|
||||
docId,
|
||||
});
|
||||
}
|
||||
|
||||
if (origin === 'self') {
|
||||
this.upstream.applyUpdate(update, 'external', docId);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubdocs = ({ added }: { added: Set<YDoc> }) => {
|
||||
added.forEach(subdoc => {
|
||||
this.setupListener(subdoc.guid);
|
||||
});
|
||||
};
|
||||
|
||||
doc.subdocs.forEach(subdoc => {
|
||||
this.setupListener(subdoc.guid);
|
||||
});
|
||||
|
||||
// listen to upstream update
|
||||
this.upstream.yDoc.on('update', onUpstreamUpdate);
|
||||
doc.on('update', (update, origin) => {
|
||||
onSelfUpdate(update, origin).catch(err => {
|
||||
logger.error(err);
|
||||
});
|
||||
});
|
||||
doc.on('subdocs', onSubdocs);
|
||||
|
||||
this.unsubscribers.add(() => {
|
||||
this.upstream.yDoc.off('update', onUpstreamUpdate);
|
||||
doc.off('update', (update, origin) => {
|
||||
onSelfUpdate(update, origin).catch(err => {
|
||||
logger.error(err);
|
||||
});
|
||||
});
|
||||
doc.off('subdocs', onSubdocs);
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.firstConnected) {
|
||||
return;
|
||||
}
|
||||
this.firstConnected = true;
|
||||
this.setupListener();
|
||||
// apply all updates from upstream
|
||||
// we assume here that the upstream ydoc is already sync'ed
|
||||
const syncUpstreamDoc = (docId?: string) => {
|
||||
const update = this.upstream.getDocAsUpdates(docId);
|
||||
if (update) {
|
||||
this.applyUpdate(update, 'upstream');
|
||||
}
|
||||
};
|
||||
syncUpstreamDoc();
|
||||
this.upstream.yDoc.subdocs.forEach(subdoc => {
|
||||
syncUpstreamDoc(subdoc.guid);
|
||||
});
|
||||
}
|
||||
|
||||
applyUpdate = (
|
||||
data: Uint8Array,
|
||||
origin: YOrigin = 'upstream',
|
||||
docId?: string
|
||||
) => {
|
||||
const doc = this.getDoc(docId);
|
||||
if (doc) {
|
||||
applyUpdate(this.yDoc, data, origin);
|
||||
} else {
|
||||
logger.warn(
|
||||
'[SecondaryWorkspaceSQLiteDB] applyUpdate: doc not found',
|
||||
docId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: have a better solution to handle blobs
|
||||
async syncBlobs() {
|
||||
await this.run(async () => {
|
||||
// skip if upstream db is not connected (maybe it is already closed)
|
||||
const blobsKeys = await this.getBlobKeys();
|
||||
if (!this.upstream.db || this.upstream.db?.isClose) {
|
||||
return;
|
||||
}
|
||||
const upstreamBlobsKeys = await this.upstream.getBlobKeys();
|
||||
// put every missing blob to upstream
|
||||
for (const key of blobsKeys) {
|
||||
if (!upstreamBlobsKeys.includes(key)) {
|
||||
const blob = await this.getBlob(key);
|
||||
if (blob) {
|
||||
await this.upstream.addBlob(key, blob);
|
||||
logger.debug('syncBlobs', this.workspaceId, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* pull from external DB file and apply to embedded yDoc
|
||||
* workflow:
|
||||
* - connect to external db
|
||||
* - get updates
|
||||
* - apply updates to local yDoc
|
||||
* - get blobs and put new blobs to upstream
|
||||
* - disconnect
|
||||
*/
|
||||
async pull() {
|
||||
const start = performance.now();
|
||||
assert(this.upstream.db, 'upstream db should be connected');
|
||||
const rows = await this.run(async () => {
|
||||
// TODO: no need to get all updates, just get the latest ones (using a cursor, etc)?
|
||||
await this.syncBlobs();
|
||||
return await this.getAllUpdates();
|
||||
});
|
||||
|
||||
if (!rows || this.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// apply root doc first
|
||||
rows.forEach(row => {
|
||||
if (!row.docId) {
|
||||
this.applyUpdate(row.data, 'self');
|
||||
}
|
||||
});
|
||||
|
||||
rows.forEach(row => {
|
||||
if (row.docId) {
|
||||
this.applyUpdate(row.data, 'self', row.docId);
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
'pull external updates',
|
||||
this.path,
|
||||
rows.length,
|
||||
(performance.now() - start).toFixed(2),
|
||||
'ms'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSecondaryWorkspaceDBPath(workspaceId: string) {
|
||||
const meta = await getWorkspaceMeta(workspaceId);
|
||||
return meta?.secondaryDBPath;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export const dbSubjects = {
|
||||
externalUpdate$: new Subject<{
|
||||
workspaceId: string;
|
||||
update: Uint8Array;
|
||||
docId?: string;
|
||||
}>(),
|
||||
};
|
||||
@@ -1,20 +1,16 @@
|
||||
import type { InsertRow } from '@affine/native';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { Subject } from 'rxjs';
|
||||
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import type { YOrigin } from '../type';
|
||||
import { getWorkspaceMeta } from '../workspace/meta';
|
||||
import { BaseSQLiteAdapter } from './base-db-adapter';
|
||||
import { dbSubjects } from './subjects';
|
||||
import { mergeUpdate } from './merge-update';
|
||||
|
||||
const TRIM_SIZE = 500;
|
||||
|
||||
export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
role = 'primary';
|
||||
yDoc = new YDoc();
|
||||
firstConnected = false;
|
||||
|
||||
update$ = new Subject<void>();
|
||||
|
||||
@@ -27,131 +23,30 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
|
||||
override async destroy() {
|
||||
await super.destroy();
|
||||
this.yDoc.destroy();
|
||||
|
||||
// when db is closed, we can safely remove it from ensure-db list
|
||||
this.update$.complete();
|
||||
this.firstConnected = false;
|
||||
}
|
||||
|
||||
getDoc(docId?: string) {
|
||||
if (!docId) {
|
||||
return this.yDoc;
|
||||
}
|
||||
// this should be pretty fast and we don't need to cache it
|
||||
for (const subdoc of this.yDoc.subdocs) {
|
||||
if (subdoc.guid === docId) {
|
||||
return subdoc;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getWorkspaceName = () => {
|
||||
return this.yDoc.getMap('meta').get('name') as string;
|
||||
getWorkspaceName = async () => {
|
||||
const ydoc = new YDoc();
|
||||
const updates = await this.getUpdates();
|
||||
updates.forEach(update => {
|
||||
applyUpdate(ydoc, update.data);
|
||||
});
|
||||
return ydoc.getMap('meta').get('name') as string;
|
||||
};
|
||||
|
||||
setupListener(docId?: string) {
|
||||
logger.debug('WorkspaceSQLiteDB:setupListener', this.workspaceId, docId);
|
||||
const doc = this.getDoc(docId);
|
||||
if (doc) {
|
||||
const onUpdate = async (update: Uint8Array, origin: YOrigin) => {
|
||||
logger.debug(
|
||||
'WorkspaceSQLiteDB:onUpdate',
|
||||
this.workspaceId,
|
||||
docId,
|
||||
update.length
|
||||
);
|
||||
const insertRows = [{ data: update, docId }];
|
||||
if (origin === 'renderer') {
|
||||
await this.addUpdateToSQLite(insertRows);
|
||||
} else if (origin === 'external') {
|
||||
dbSubjects.externalUpdate$.next({
|
||||
workspaceId: this.workspaceId,
|
||||
update,
|
||||
docId,
|
||||
});
|
||||
await this.addUpdateToSQLite(insertRows);
|
||||
logger.debug('external update', this.workspaceId);
|
||||
}
|
||||
};
|
||||
doc.subdocs.forEach(subdoc => {
|
||||
this.setupListener(subdoc.guid);
|
||||
});
|
||||
const onSubdocs = ({ added }: { added: Set<YDoc> }) => {
|
||||
logger.info('onSubdocs', this.workspaceId, docId, added);
|
||||
added.forEach(subdoc => {
|
||||
this.setupListener(subdoc.guid);
|
||||
});
|
||||
};
|
||||
|
||||
doc.on('update', (update, origin) => {
|
||||
onUpdate(update, origin).catch(err => {
|
||||
logger.error(err);
|
||||
});
|
||||
});
|
||||
doc.on('subdocs', onSubdocs);
|
||||
} else {
|
||||
logger.error('setupListener: doc not found', docId);
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
const db = await super.connectIfNeeded();
|
||||
|
||||
if (!this.firstConnected) {
|
||||
this.setupListener();
|
||||
}
|
||||
|
||||
const updates = await this.getAllUpdates();
|
||||
|
||||
// apply root first (without ID).
|
||||
// subdoc will be available after root is applied
|
||||
updates.forEach(update => {
|
||||
if (!update.docId) {
|
||||
this.applyUpdate(update.data, 'self');
|
||||
}
|
||||
});
|
||||
|
||||
// then, for all subdocs, apply the updates
|
||||
updates.forEach(update => {
|
||||
if (update.docId) {
|
||||
this.applyUpdate(update.data, 'self', update.docId);
|
||||
}
|
||||
});
|
||||
|
||||
this.firstConnected = true;
|
||||
this.update$.next();
|
||||
|
||||
await this.tryTrim();
|
||||
return db;
|
||||
}
|
||||
|
||||
// unlike getUpdates, this will return updates in yDoc
|
||||
getDocAsUpdates = (docId?: string) => {
|
||||
const doc = docId ? this.getDoc(docId) : this.yDoc;
|
||||
if (doc) {
|
||||
return encodeStateAsUpdate(doc);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// non-blocking and use yDoc to validate the update
|
||||
// after that, the update is added to the db
|
||||
applyUpdate = (
|
||||
data: Uint8Array,
|
||||
origin: YOrigin = 'renderer',
|
||||
docId?: string
|
||||
) => {
|
||||
// todo: trim the updates when the number of records is too large
|
||||
// 1. store the current ydoc state in the db
|
||||
// 2. then delete the old updates
|
||||
// yjs-idb will always trim the db for the first time after DB is loaded
|
||||
const doc = this.getDoc(docId);
|
||||
if (doc) {
|
||||
applyUpdate(doc, data, origin);
|
||||
} else {
|
||||
logger.warn('[WorkspaceSQLiteDB] applyUpdate: doc not found', docId);
|
||||
}
|
||||
// getUpdates then encode
|
||||
getDocAsUpdates = async (docId?: string) => {
|
||||
const updates = await this.getUpdates(docId);
|
||||
return mergeUpdate(updates.map(row => row.data));
|
||||
};
|
||||
|
||||
override async addBlob(key: string, value: Uint8Array) {
|
||||
@@ -167,28 +62,21 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
|
||||
override async addUpdateToSQLite(data: InsertRow[]) {
|
||||
this.update$.next();
|
||||
data.forEach(row => {
|
||||
this.trimWhenNecessary(row.docId)?.catch(err => {
|
||||
logger.error('trimWhenNecessary failed', err);
|
||||
});
|
||||
});
|
||||
await super.addUpdateToSQLite(data);
|
||||
}
|
||||
|
||||
trimWhenNecessary = debounce(async (docId?: string) => {
|
||||
if (this.firstConnected) {
|
||||
const count = (await this.db?.getUpdatesCount(docId)) ?? 0;
|
||||
if (count > TRIM_SIZE) {
|
||||
logger.debug(`trim ${this.workspaceId}:${docId} ${count}`);
|
||||
const update = this.getDocAsUpdates(docId);
|
||||
if (update) {
|
||||
const insertRows = [{ data: update, docId }];
|
||||
await this.db?.replaceUpdates(docId, insertRows);
|
||||
logger.debug(`trim ${this.workspaceId}:${docId} successfully`);
|
||||
}
|
||||
private readonly tryTrim = async (docId?: string) => {
|
||||
const count = (await this.db?.getUpdatesCount(docId)) ?? 0;
|
||||
if (count > TRIM_SIZE) {
|
||||
logger.debug(`trim ${this.workspaceId}:${docId} ${count}`);
|
||||
const update = await this.getDocAsUpdates(docId);
|
||||
if (update) {
|
||||
const insertRows = [{ data: update, docId }];
|
||||
await this.db?.replaceUpdates(docId, insertRows);
|
||||
logger.debug(`trim ${this.workspaceId}:${docId} successfully`);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
}
|
||||
|
||||
export async function openWorkspaceDatabase(workspaceId: string) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { ValidationResult } from '@affine/native';
|
||||
import { WorkspaceVersion } from '@toeverything/infra/blocksuite';
|
||||
import fs from 'fs-extra';
|
||||
@@ -11,10 +9,9 @@ import {
|
||||
migrateToLatest,
|
||||
migrateToSubdocAndReplaceDatabase,
|
||||
} from '../db/migration';
|
||||
import type { WorkspaceSQLiteDB } from '../db/workspace-db-adapter';
|
||||
import { logger } from '../logger';
|
||||
import { mainRPC } from '../main-rpc';
|
||||
import { listWorkspaces, storeWorkspaceMeta } from '../workspace';
|
||||
import { storeWorkspaceMeta } from '../workspace';
|
||||
import {
|
||||
getWorkspaceDBPath,
|
||||
getWorkspaceMeta,
|
||||
@@ -47,12 +44,6 @@ export interface SelectDBFileLocationResult {
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
export interface MoveDBFileResult {
|
||||
filePath?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
// provide a backdoor to set dialog path for testing in playwright
|
||||
export interface FakeDialogResult {
|
||||
canceled?: boolean;
|
||||
@@ -68,7 +59,7 @@ export async function revealDBFile(workspaceId: string) {
|
||||
if (!meta) {
|
||||
return;
|
||||
}
|
||||
await mainRPC.showItemInFolder(meta.secondaryDBPath ?? meta.mainDBPath);
|
||||
await mainRPC.showItemInFolder(meta.mainDBPath);
|
||||
}
|
||||
|
||||
// result will be used in the next call to showOpenDialog
|
||||
@@ -120,7 +111,10 @@ export async function saveDBFileAs(
|
||||
name: '',
|
||||
},
|
||||
],
|
||||
defaultPath: getDefaultDBFileName(db.getWorkspaceName(), workspaceId),
|
||||
defaultPath: getDefaultDBFileName(
|
||||
await db.getWorkspaceName(),
|
||||
workspaceId
|
||||
),
|
||||
message: 'Save Workspace as a SQLite Database file',
|
||||
}));
|
||||
const filePath = ret.filePath;
|
||||
@@ -213,11 +207,6 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
|
||||
return { error: 'DB_FILE_PATH_INVALID' };
|
||||
}
|
||||
|
||||
if (await dbFileAlreadyLoaded(originalPath)) {
|
||||
logger.warn('loadDBFile: db file already loaded');
|
||||
return { error: 'DB_FILE_ALREADY_LOADED' };
|
||||
}
|
||||
|
||||
const { SqliteConnection } = await import('@affine/native');
|
||||
|
||||
const validationResult = await SqliteConnection.validate(originalPath);
|
||||
@@ -294,100 +283,3 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the user clicks the "Move" button in the "Move Workspace Storage" setting.
|
||||
*
|
||||
* It will
|
||||
* - copy the source db file to a new location
|
||||
* - remove the old db external file
|
||||
* - update the external db file path in the workspace meta
|
||||
* - return the new file path
|
||||
*/
|
||||
export async function moveDBFile(
|
||||
workspaceId: string,
|
||||
dbFileDir?: string
|
||||
): Promise<MoveDBFileResult> {
|
||||
let db: WorkspaceSQLiteDB | null = null;
|
||||
try {
|
||||
db = await ensureSQLiteDB(workspaceId);
|
||||
const meta = await getWorkspaceMeta(workspaceId);
|
||||
|
||||
const oldDir = meta.secondaryDBPath
|
||||
? path.dirname(meta.secondaryDBPath)
|
||||
: null;
|
||||
const defaultDir = oldDir ?? (await mainRPC.getPath('documents'));
|
||||
|
||||
const newName = getDefaultDBFileName(db.getWorkspaceName(), workspaceId);
|
||||
|
||||
const newDirPath =
|
||||
dbFileDir ??
|
||||
(
|
||||
getFakedResult() ??
|
||||
(await mainRPC.showOpenDialog({
|
||||
properties: ['openDirectory'],
|
||||
title: 'Move Workspace Storage',
|
||||
buttonLabel: 'Move',
|
||||
defaultPath: defaultDir,
|
||||
message: 'Move Workspace storage file',
|
||||
}))
|
||||
).filePaths?.[0];
|
||||
|
||||
// skips if
|
||||
// - user canceled the dialog
|
||||
// - user selected the same dir
|
||||
if (!newDirPath || newDirPath === oldDir) {
|
||||
return {
|
||||
canceled: true,
|
||||
};
|
||||
}
|
||||
|
||||
const newFilePath = path.join(newDirPath, newName);
|
||||
|
||||
if (await fs.pathExists(newFilePath)) {
|
||||
return {
|
||||
error: 'FILE_ALREADY_EXISTS',
|
||||
};
|
||||
}
|
||||
|
||||
logger.info(`[moveDBFile] copy ${meta.mainDBPath} -> ${newFilePath}`);
|
||||
|
||||
await fs.copy(meta.mainDBPath, newFilePath);
|
||||
|
||||
// remove the old db file, but we don't care if it fails
|
||||
if (meta.secondaryDBPath) {
|
||||
await fs
|
||||
.remove(meta.secondaryDBPath)
|
||||
.then(() => {
|
||||
logger.info(`[moveDBFile] removed ${meta.secondaryDBPath}`);
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error(
|
||||
`[moveDBFile] remove ${meta.secondaryDBPath} failed`,
|
||||
err
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// update meta
|
||||
await storeWorkspaceMeta(workspaceId, {
|
||||
secondaryDBPath: newFilePath,
|
||||
});
|
||||
|
||||
return {
|
||||
filePath: newFilePath,
|
||||
};
|
||||
} catch (err) {
|
||||
await db?.destroy();
|
||||
logger.error('[moveDBFile]', err);
|
||||
return {
|
||||
error: 'UNKNOWN_ERROR',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function dbFileAlreadyLoaded(path: string) {
|
||||
const meta = await listWorkspaces();
|
||||
const paths = meta.map(m => m[1].secondaryDBPath);
|
||||
return paths.includes(path);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
loadDBFile,
|
||||
moveDBFile,
|
||||
revealDBFile,
|
||||
saveDBFileAs,
|
||||
selectDBFileLocation,
|
||||
@@ -17,9 +16,6 @@ export const dialogHandlers = {
|
||||
saveDBFileAs: async (workspaceId: string) => {
|
||||
return saveDBFileAs(workspaceId);
|
||||
},
|
||||
moveDBFile: (workspaceId: string, dbFileLocation?: string) => {
|
||||
return moveDBFile(workspaceId, dbFileLocation);
|
||||
},
|
||||
selectDBFileLocation: async () => {
|
||||
return selectDBFileLocation();
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ function setupRendererConnection(rendererPort: Electron.MessagePortMain) {
|
||||
try {
|
||||
const start = performance.now();
|
||||
const result = await handler(...args);
|
||||
logger.info(
|
||||
logger.debug(
|
||||
'[async-api]',
|
||||
`${namespace}.${name}`,
|
||||
args.filter(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export interface WorkspaceMeta {
|
||||
id: string;
|
||||
mainDBPath: string;
|
||||
secondaryDBPath?: string; // assume there will be only one
|
||||
}
|
||||
|
||||
export type YOrigin = 'self' | 'external' | 'upstream' | 'renderer';
|
||||
|
||||
@@ -52,26 +52,12 @@ export async function getWorkspaceMeta(
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
) {
|
||||
// since not meta is found, we will migrate symlinked db file if needed
|
||||
await fs.ensureDir(basePath);
|
||||
const dbPath = await getWorkspaceDBPath(workspaceId);
|
||||
|
||||
// todo: remove this after migration (in stable version)
|
||||
const realDBPath = (await fs
|
||||
.access(dbPath)
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
? await fs.realpath(dbPath)
|
||||
: dbPath;
|
||||
const isLink = realDBPath !== dbPath;
|
||||
if (isLink) {
|
||||
await fs.copy(realDBPath, dbPath);
|
||||
}
|
||||
// create one if not exists
|
||||
const meta = {
|
||||
id: workspaceId,
|
||||
mainDBPath: dbPath,
|
||||
secondaryDBPath: isLink ? realDBPath : undefined,
|
||||
};
|
||||
await fs.writeJSON(metaPath, meta);
|
||||
return meta;
|
||||
|
||||
@@ -99,47 +99,3 @@ test('db should be removed in db$Map after destroyed', async () => {
|
||||
await setTimeout(100);
|
||||
expect(db$Map.has(workspaceId)).toBe(false);
|
||||
});
|
||||
|
||||
// we have removed secondary db feature
|
||||
test.skip('if db has a secondary db path, we should also poll that', async () => {
|
||||
const { ensureSQLiteDB } = await import(
|
||||
'@affine/electron/helper/db/ensure-db'
|
||||
);
|
||||
const { storeWorkspaceMeta } = await import(
|
||||
'@affine/electron/helper/workspace'
|
||||
);
|
||||
const workspaceId = v4();
|
||||
await storeWorkspaceMeta(workspaceId, {
|
||||
secondaryDBPath: path.join(tmpDir, 'secondary.db'),
|
||||
});
|
||||
|
||||
const db = await ensureSQLiteDB(workspaceId);
|
||||
|
||||
await setTimeout(10);
|
||||
|
||||
expect(constructorStub).toBeCalledTimes(1);
|
||||
expect(constructorStub).toBeCalledWith(path.join(tmpDir, 'secondary.db'), db);
|
||||
|
||||
// if secondary meta is changed
|
||||
await storeWorkspaceMeta(workspaceId, {
|
||||
secondaryDBPath: path.join(tmpDir, 'secondary2.db'),
|
||||
});
|
||||
|
||||
// wait the async `db.destroy()` to be called
|
||||
await setTimeout(100);
|
||||
expect(constructorStub).toBeCalledTimes(2);
|
||||
expect(destroyStub).toBeCalledTimes(1);
|
||||
|
||||
// if secondary meta is changed (but another workspace)
|
||||
await storeWorkspaceMeta(v4(), {
|
||||
secondaryDBPath: path.join(tmpDir, 'secondary3.db'),
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
expect(constructorStub).toBeCalledTimes(2);
|
||||
expect(destroyStub).toBeCalledTimes(1);
|
||||
|
||||
// if primary is destroyed, secondary should also be destroyed
|
||||
await db.destroy();
|
||||
await setTimeout(100);
|
||||
expect(destroyStub).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { dbSubjects } from '@affine/electron/helper/db/subjects';
|
||||
import { removeWithRetry } from '@affine-test/kit/utils/utils';
|
||||
import fs from 'fs-extra';
|
||||
import { v4 } from 'uuid';
|
||||
import { afterAll, afterEach, beforeAll, expect, test, vi } from 'vitest';
|
||||
import { Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
const appDataPath = path.join(tmpDir, 'app-data');
|
||||
@@ -26,31 +24,6 @@ afterAll(() => {
|
||||
vi.doUnmock('@affine/electron/helper/main-rpc');
|
||||
});
|
||||
|
||||
let testYDoc: YDoc;
|
||||
let testYSubDoc: YDoc;
|
||||
|
||||
function getTestUpdates() {
|
||||
testYDoc = new YDoc();
|
||||
const yText = testYDoc.getText('test');
|
||||
yText.insert(0, 'hello');
|
||||
|
||||
testYSubDoc = new YDoc();
|
||||
testYDoc.getMap('subdocs').set('test-subdoc', testYSubDoc);
|
||||
|
||||
const updates = encodeStateAsUpdate(testYDoc);
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
function getTestSubDocUpdates() {
|
||||
const yText = testYSubDoc.getText('test');
|
||||
yText.insert(0, 'hello');
|
||||
|
||||
const updates = encodeStateAsUpdate(testYSubDoc);
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
test('can create new db file if not exists', async () => {
|
||||
const { openWorkspaceDatabase } = await import(
|
||||
'@affine/electron/helper/db/workspace-db-adapter'
|
||||
@@ -66,82 +39,6 @@ test('can create new db file if not exists', async () => {
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('on applyUpdate (from self), will not trigger update', async () => {
|
||||
const { openWorkspaceDatabase } = await import(
|
||||
'@affine/electron/helper/db/workspace-db-adapter'
|
||||
);
|
||||
const workspaceId = v4();
|
||||
const onUpdate = vi.fn();
|
||||
|
||||
const db = await openWorkspaceDatabase(workspaceId);
|
||||
db.update$.subscribe(onUpdate);
|
||||
db.applyUpdate(getTestUpdates(), 'self');
|
||||
expect(onUpdate).not.toHaveBeenCalled();
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('on applyUpdate (from renderer), will trigger update', async () => {
|
||||
const { openWorkspaceDatabase } = await import(
|
||||
'@affine/electron/helper/db/workspace-db-adapter'
|
||||
);
|
||||
const workspaceId = v4();
|
||||
const onUpdate = vi.fn();
|
||||
const onExternalUpdate = vi.fn();
|
||||
|
||||
const db = await openWorkspaceDatabase(workspaceId);
|
||||
db.update$.subscribe(onUpdate);
|
||||
const sub = dbSubjects.externalUpdate$.subscribe(onExternalUpdate);
|
||||
db.applyUpdate(getTestUpdates(), 'renderer');
|
||||
expect(onUpdate).toHaveBeenCalled();
|
||||
sub.unsubscribe();
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('on applyUpdate (from renderer, subdoc), will trigger update', async () => {
|
||||
const { openWorkspaceDatabase } = await import(
|
||||
'@affine/electron/helper/db/workspace-db-adapter'
|
||||
);
|
||||
const workspaceId = v4();
|
||||
const onUpdate = vi.fn();
|
||||
const insertUpdates = vi.fn();
|
||||
|
||||
const db = await openWorkspaceDatabase(workspaceId);
|
||||
db.applyUpdate(getTestUpdates(), 'renderer');
|
||||
|
||||
db.db!.insertUpdates = insertUpdates;
|
||||
db.update$.subscribe(onUpdate);
|
||||
|
||||
const subdocUpdates = getTestSubDocUpdates();
|
||||
db.applyUpdate(subdocUpdates, 'renderer', testYSubDoc.guid);
|
||||
|
||||
expect(onUpdate).toHaveBeenCalled();
|
||||
expect(insertUpdates).toHaveBeenCalledWith([
|
||||
{
|
||||
docId: testYSubDoc.guid,
|
||||
data: subdocUpdates,
|
||||
},
|
||||
]);
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('on applyUpdate (from external), will trigger update & send external update event', async () => {
|
||||
const { openWorkspaceDatabase } = await import(
|
||||
'@affine/electron/helper/db/workspace-db-adapter'
|
||||
);
|
||||
const workspaceId = v4();
|
||||
const onUpdate = vi.fn();
|
||||
const onExternalUpdate = vi.fn();
|
||||
|
||||
const db = await openWorkspaceDatabase(workspaceId);
|
||||
db.update$.subscribe(onUpdate);
|
||||
const sub = dbSubjects.externalUpdate$.subscribe(onExternalUpdate);
|
||||
db.applyUpdate(getTestUpdates(), 'external');
|
||||
expect(onUpdate).toHaveBeenCalled();
|
||||
expect(onExternalUpdate).toHaveBeenCalled();
|
||||
sub.unsubscribe();
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('on destroy, check if resources have been released', async () => {
|
||||
const { openWorkspaceDatabase } = await import(
|
||||
'@affine/electron/helper/db/workspace-db-adapter'
|
||||
|
||||
@@ -127,7 +127,6 @@ describe('getWorkspaceMeta', () => {
|
||||
expect(await getWorkspaceMeta(workspaceId)).toEqual({
|
||||
id: workspaceId,
|
||||
mainDBPath: path.join(workspacePath, 'storage.db'),
|
||||
secondaryDBPath: sourcePath,
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -151,11 +150,4 @@ test('storeWorkspaceMeta', async () => {
|
||||
expect(await fs.readJSON(path.join(workspacePath, 'meta.json'))).toEqual(
|
||||
meta
|
||||
);
|
||||
await storeWorkspaceMeta(workspaceId, {
|
||||
secondaryDBPath: path.join(tmpDir, 'test.db'),
|
||||
});
|
||||
expect(await fs.readJSON(path.join(workspacePath, 'meta.json'))).toEqual({
|
||||
...meta,
|
||||
secondaryDBPath: path.join(tmpDir, 'test.db'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@aws-sdk/client-s3": "3.537.0",
|
||||
"@blocksuite/presets": "0.14.0-canary-202404260628-ddb1941",
|
||||
"@blocksuite/presets": "0.14.0-canary-202404280529-c8e5f89",
|
||||
"@clack/core": "^0.3.4",
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@magic-works/i18n-codegen": "^0.5.0",
|
||||
|
||||
@@ -17,7 +17,6 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enablePreloading: true,
|
||||
enableNewSettingModal: true,
|
||||
enableNewSettingUnstableApi: false,
|
||||
enableMoveDatabase: false,
|
||||
enableCloud: true,
|
||||
enableCaptcha: true,
|
||||
enableEnhanceShareMode: false,
|
||||
@@ -57,7 +56,6 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enablePreloading: true,
|
||||
enableNewSettingModal: true,
|
||||
enableNewSettingUnstableApi: false,
|
||||
enableMoveDatabase: false,
|
||||
enableCloud: true,
|
||||
enableCaptcha: true,
|
||||
enableEnhanceShareMode: false,
|
||||
@@ -107,9 +105,6 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableEnhanceShareMode: process.env.ENABLE_ENHANCE_SHARE_MODE
|
||||
? process.env.ENABLE_ENHANCE_SHARE_MODE === 'true'
|
||||
: currentBuildPreset.enableEnhanceShareMode,
|
||||
enableMoveDatabase: process.env.ENABLE_MOVE_DATABASE
|
||||
? process.env.ENABLE_MOVE_DATABASE === 'true'
|
||||
: currentBuildPreset.enableMoveDatabase,
|
||||
enablePayment: process.env.ENABLE_PAYMENT
|
||||
? process.env.ENABLE_PAYMENT !== 'false'
|
||||
: buildFlags.mode === 'development'
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"@toeverything/infra": ["./packages/common/infra/src"],
|
||||
"@affine/native": ["./packages/frontend/native/index.d.ts"],
|
||||
"@affine/native/*": ["./packages/frontend/native/*"],
|
||||
"@affine/storage": ["./packages/backend/storage/index.d.ts"],
|
||||
"@affine/server-native": ["./packages/backend/native/index.d.ts"],
|
||||
// Development only
|
||||
"@affine/electron/*": ["./packages/frontend/electron/src/*"]
|
||||
}
|
||||
|
||||
221
yarn.lock
221
yarn.lock
@@ -173,7 +173,7 @@ __metadata:
|
||||
"@affine/env": "workspace:*"
|
||||
"@affine/templates": "workspace:*"
|
||||
"@aws-sdk/client-s3": "npm:3.537.0"
|
||||
"@blocksuite/presets": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/presets": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@clack/core": "npm:^0.3.4"
|
||||
"@clack/prompts": "npm:^0.7.0"
|
||||
"@magic-works/i18n-codegen": "npm:^0.5.0"
|
||||
@@ -226,12 +226,12 @@ __metadata:
|
||||
"@affine/electron-api": "workspace:*"
|
||||
"@affine/graphql": "workspace:*"
|
||||
"@affine/i18n": "workspace:*"
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/icons": "npm:2.1.46"
|
||||
"@blocksuite/presets": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/presets": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@dnd-kit/core": "npm:^6.1.0"
|
||||
"@dnd-kit/modifiers": "npm:^7.0.0"
|
||||
"@dnd-kit/sortable": "npm:^8.0.0"
|
||||
@@ -327,13 +327,13 @@ __metadata:
|
||||
"@affine/graphql": "workspace:*"
|
||||
"@affine/i18n": "workspace:*"
|
||||
"@affine/templates": "workspace:*"
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/icons": "npm:2.1.46"
|
||||
"@blocksuite/inline": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/presets": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/inline": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/presets": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@dnd-kit/core": "npm:^6.1.0"
|
||||
"@dnd-kit/modifiers": "npm:^7.0.0"
|
||||
"@dnd-kit/sortable": "npm:^8.0.0"
|
||||
@@ -455,10 +455,10 @@ __metadata:
|
||||
"@affine/env": "workspace:*"
|
||||
"@affine/i18n": "workspace:*"
|
||||
"@affine/native": "workspace:*"
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/presets": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/presets": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@electron-forge/cli": "npm:^7.3.0"
|
||||
"@electron-forge/core": "npm:^7.3.0"
|
||||
"@electron-forge/core-utils": "npm:^7.3.0"
|
||||
@@ -469,7 +469,7 @@ __metadata:
|
||||
"@electron-forge/plugin-auto-unpack-natives": "npm:^7.3.0"
|
||||
"@electron-forge/shared-types": "npm:^7.3.0"
|
||||
"@emotion/react": "npm:^11.11.4"
|
||||
"@pengx17/electron-forge-maker-appimage": "npm:^1.2.0"
|
||||
"@pengx17/electron-forge-maker-appimage": "npm:^1.2.1"
|
||||
"@sentry/electron": "npm:^4.22.0"
|
||||
"@sentry/esbuild-plugin": "npm:^2.16.1"
|
||||
"@sentry/react": "npm:^7.109.0"
|
||||
@@ -516,8 +516,8 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/env@workspace:packages/common/env"
|
||||
dependencies:
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
lit: "npm:^3.1.2"
|
||||
react: "npm:18.2.0"
|
||||
react-dom: "npm:18.2.0"
|
||||
@@ -644,12 +644,24 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/server-native@workspace:*, @affine/server-native@workspace:packages/backend/native":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/server-native@workspace:packages/backend/native"
|
||||
dependencies:
|
||||
"@napi-rs/cli": "npm:3.0.0-alpha.46"
|
||||
lib0: "npm:^0.2.93"
|
||||
nx: "npm:^18.2.4"
|
||||
nx-cloud: "npm:^18.0.0"
|
||||
yjs: "npm:^13.6.14"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/server@workspace:packages/backend/server":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/server@workspace:packages/backend/server"
|
||||
dependencies:
|
||||
"@affine-test/kit": "workspace:*"
|
||||
"@affine/storage": "workspace:*"
|
||||
"@affine/server-native": "workspace:*"
|
||||
"@apollo/server": "npm:^4.10.2"
|
||||
"@aws-sdk/client-s3": "npm:^3.552.0"
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "npm:^0.17.0"
|
||||
@@ -712,7 +724,6 @@ __metadata:
|
||||
dotenv: "npm:^16.4.5"
|
||||
dotenv-cli: "npm:^7.4.1"
|
||||
express: "npm:^4.19.2"
|
||||
file-type: "npm:^19.0.0"
|
||||
get-stream: "npm:^9.0.1"
|
||||
graphql: "npm:^16.8.1"
|
||||
graphql-scalars: "npm:^1.23.0"
|
||||
@@ -752,18 +763,6 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/storage@workspace:*, @affine/storage@workspace:packages/backend/storage":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/storage@workspace:packages/backend/storage"
|
||||
dependencies:
|
||||
"@napi-rs/cli": "npm:3.0.0-alpha.46"
|
||||
lib0: "npm:^0.2.93"
|
||||
nx: "npm:^18.2.4"
|
||||
nx-cloud: "npm:^18.0.0"
|
||||
yjs: "npm:^13.6.14"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/templates@workspace:*, @affine/templates@workspace:packages/frontend/templates":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/templates@workspace:packages/frontend/templates"
|
||||
@@ -3732,30 +3731,30 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@blocksuite/block-std@npm:0.14.0-canary-202404260628-ddb1941":
|
||||
version: 0.14.0-canary-202404260628-ddb1941
|
||||
resolution: "@blocksuite/block-std@npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/block-std@npm:0.14.0-canary-202404280529-c8e5f89":
|
||||
version: 0.14.0-canary-202404280529-c8e5f89
|
||||
resolution: "@blocksuite/block-std@npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
dependencies:
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
lit: "npm:^3.1.3"
|
||||
lz-string: "npm:^1.5.0"
|
||||
w3c-keyname: "npm:^2.2.8"
|
||||
zod: "npm:^3.22.4"
|
||||
peerDependencies:
|
||||
"@blocksuite/inline": 0.14.0-canary-202404260628-ddb1941
|
||||
"@blocksuite/store": 0.14.0-canary-202404260628-ddb1941
|
||||
checksum: 10/bafca28b660194351bb136687b1e881d1b1b5ca8476742303ca18a34a7bdf262f46c21c6b037074a3b83affe482395aef727571f3011477c57740cbb9ef51cb5
|
||||
"@blocksuite/inline": 0.14.0-canary-202404280529-c8e5f89
|
||||
"@blocksuite/store": 0.14.0-canary-202404280529-c8e5f89
|
||||
checksum: 10/ed48717fc653f8fcdcb513342de54ba063fbbc28ea85bac4a342c7926a96776f16fd8ff7d26921c6856767723994c77a35be1a10050d191c3063a67a5f0c712a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@blocksuite/blocks@npm:0.14.0-canary-202404260628-ddb1941":
|
||||
version: 0.14.0-canary-202404260628-ddb1941
|
||||
resolution: "@blocksuite/blocks@npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/blocks@npm:0.14.0-canary-202404280529-c8e5f89":
|
||||
version: 0.14.0-canary-202404280529-c8e5f89
|
||||
resolution: "@blocksuite/blocks@npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
dependencies:
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/inline": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/inline": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@dotlottie/player-component": "npm:^2.7.12"
|
||||
"@fal-ai/serverless-client": "npm:^0.9.3"
|
||||
"@floating-ui/dom": "npm:^1.6.3"
|
||||
@@ -3794,16 +3793,16 @@ __metadata:
|
||||
unified: "npm:^11.0.4"
|
||||
webfontloader: "npm:^1.6.28"
|
||||
zod: "npm:^3.22.4"
|
||||
checksum: 10/8860fa8e2f858b07ca36698025994ece9591c775946bd2e33581b339c2a52e546e670603dd18d6dedfa4ebd60a144155be8f154abfeaf82979b31fcf5bfa65bf
|
||||
checksum: 10/48e5232b2e720492ad77616ffa2cfbaf045e10af5925534e034dfda7310d6abc9a316fbb76ea456292f3612aaec2cda4e475e7465ae474deabcf20d5ded6e040
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@blocksuite/global@npm:0.14.0-canary-202404260628-ddb1941":
|
||||
version: 0.14.0-canary-202404260628-ddb1941
|
||||
resolution: "@blocksuite/global@npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global@npm:0.14.0-canary-202404280529-c8e5f89":
|
||||
version: 0.14.0-canary-202404280529-c8e5f89
|
||||
resolution: "@blocksuite/global@npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
dependencies:
|
||||
zod: "npm:^3.22.4"
|
||||
checksum: 10/cafdfbece39027dcd38ed77c61718c4d45718e76c255dd652ab51f6ecee6b6e318a6e6a081ec1ec79af88694d413e615351b207b1bd6e907fa52642a54ac759d
|
||||
checksum: 10/14f55505be9497db26c651454eb2d52336f525b34f916d6aee8da7921d504ac64ed7db30c174188ff83730b5fab58ce39cde4d9c1409ac1c6964e17684164795
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3817,45 +3816,45 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@blocksuite/inline@npm:0.14.0-canary-202404260628-ddb1941":
|
||||
version: 0.14.0-canary-202404260628-ddb1941
|
||||
resolution: "@blocksuite/inline@npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/inline@npm:0.14.0-canary-202404280529-c8e5f89":
|
||||
version: 0.14.0-canary-202404280529-c8e5f89
|
||||
resolution: "@blocksuite/inline@npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
dependencies:
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
zod: "npm:^3.22.4"
|
||||
peerDependencies:
|
||||
lit: ^3.1.1
|
||||
yjs: ^13
|
||||
checksum: 10/79f00d0761fe92868a09201b32dd7c347a625e7bfa14a17d498326b343b0cb7674a8ee7e27af2e435f1d19171b564f20aa1f6e81e299e8849da40852782cf78c
|
||||
checksum: 10/ba893ce944dc9f1acf37d123d34a3473f2e9e2faed75897ca609fcacf5e0628f3fb1e569eab3a641cffd3b1ffc5fbc294ebc7fe0d4f3c784f2d0174675527c79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@blocksuite/presets@npm:0.14.0-canary-202404260628-ddb1941":
|
||||
version: 0.14.0-canary-202404260628-ddb1941
|
||||
resolution: "@blocksuite/presets@npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/presets@npm:0.14.0-canary-202404280529-c8e5f89":
|
||||
version: 0.14.0-canary-202404280529-c8e5f89
|
||||
resolution: "@blocksuite/presets@npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
dependencies:
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/inline": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/inline": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@dotlottie/player-component": "npm:^2.7.12"
|
||||
"@fal-ai/serverless-client": "npm:^0.9.3"
|
||||
"@floating-ui/dom": "npm:^1.6.3"
|
||||
"@toeverything/theme": "npm:^0.7.29"
|
||||
lit: "npm:^3.1.3"
|
||||
openai: "npm:^4.37.1"
|
||||
checksum: 10/e0d998d93f5becf17f7883ea5e1af18a0c0235c997e17ca498597b5b695b53b199eb20b63b8ca924cb1e9501777c85d0393ae52c57e7bd04087beb09539a0313
|
||||
checksum: 10/6f427982c25288578e052e73e7c3400f0c1fbb8354d8ab043d34da5e7ccb9d4f20a42bd85c92cdcdb607e0e1173b70da4f90b73bdce8fe7e75e1544d570bb23c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@blocksuite/store@npm:0.14.0-canary-202404260628-ddb1941":
|
||||
version: 0.14.0-canary-202404260628-ddb1941
|
||||
resolution: "@blocksuite/store@npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/store@npm:0.14.0-canary-202404280529-c8e5f89":
|
||||
version: 0.14.0-canary-202404280529-c8e5f89
|
||||
resolution: "@blocksuite/store@npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
dependencies:
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/inline": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/sync": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/inline": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/sync": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@types/flexsearch": "npm:^0.7.6"
|
||||
flexsearch: "npm:0.7.43"
|
||||
idb-keyval: "npm:^6.2.1"
|
||||
@@ -3867,20 +3866,20 @@ __metadata:
|
||||
zod: "npm:^3.22.4"
|
||||
peerDependencies:
|
||||
yjs: ^13
|
||||
checksum: 10/fc97033a7e08922e3e9e5812df60e402c7824f048fc1cbea63e616d407e4b278549ba7465cac0c4934eafc091a461cec17fa38a99837c807627f9aed14ae3129
|
||||
checksum: 10/f0ed64763e4e5277f598d64ae555b7658ed7db9ece7389b1cc5dcd80622326d44f7a7a7cb86b790762e41cfd0fd53e9a4163e53d9a44b80d5b3b8d5fc6f7d9dc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@blocksuite/sync@npm:0.14.0-canary-202404260628-ddb1941":
|
||||
version: 0.14.0-canary-202404260628-ddb1941
|
||||
resolution: "@blocksuite/sync@npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/sync@npm:0.14.0-canary-202404280529-c8e5f89":
|
||||
version: 0.14.0-canary-202404280529-c8e5f89
|
||||
resolution: "@blocksuite/sync@npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
dependencies:
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
idb: "npm:^8.0.0"
|
||||
y-protocols: "npm:^1.0.6"
|
||||
peerDependencies:
|
||||
yjs: ^13
|
||||
checksum: 10/749bf6cbe97bddc5d5d306f02861a517fa50fbdb162798c83b5eb0839c86572088db88e93bc97c1af767b63e37de9b04c2196e0af132aa88c30e19cbb7f19611
|
||||
checksum: 10/45960ea9fc4aed445f5517539468f670e8d1eb8730ab567e0f98716a9c42aeb1ecc85717fd5fceb36d5aa40ef6d52435ce17a9f952f772d73879ee169332b2d9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10198,14 +10197,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@pengx17/electron-forge-maker-appimage@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@pengx17/electron-forge-maker-appimage@npm:1.2.0"
|
||||
"@pengx17/electron-forge-maker-appimage@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "@pengx17/electron-forge-maker-appimage@npm:1.2.1"
|
||||
dependencies:
|
||||
"@electron-forge/maker-base": "npm:^7.3.0"
|
||||
"@electron-forge/shared-types": "npm:^7.3.0"
|
||||
app-builder-lib: "npm:^24.13.3"
|
||||
checksum: 10/f5e8927810b5381462ec2cde8fcbbaab74b66e025e549d49707c1d855a9618c1b88bf136a4a0df9bc2b80a19ea136443115c462feb2a5b8b0311ec6c6c0ea1fa
|
||||
checksum: 10/632c243dd6d0ee61d17741b212c9fd2b201ee4dc05ffc244e3d14fa0f7af368546c533612145367bdf61563d03240e3464f5a5f22028a16de454cabb8fbe010b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14409,11 +14408,11 @@ __metadata:
|
||||
"@affine/debug": "workspace:*"
|
||||
"@affine/env": "workspace:*"
|
||||
"@affine/templates": "workspace:*"
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/presets": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/block-std": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/presets": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@datastructures-js/binary-search-tree": "npm:^5.3.2"
|
||||
"@testing-library/react": "npm:^15.0.0"
|
||||
async-call-rpc: "npm:^6.4.0"
|
||||
@@ -14464,9 +14463,9 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@toeverything/y-indexeddb@workspace:packages/common/y-indexeddb"
|
||||
dependencies:
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
fake-indexeddb: "npm:^5.0.2"
|
||||
idb: "npm:^8.0.0"
|
||||
nanoid: "npm:^5.0.7"
|
||||
@@ -22536,17 +22535,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-type@npm:^19.0.0":
|
||||
version: 19.0.0
|
||||
resolution: "file-type@npm:19.0.0"
|
||||
dependencies:
|
||||
readable-web-to-node-stream: "npm:^3.0.2"
|
||||
strtok3: "npm:^7.0.0"
|
||||
token-types: "npm:^5.0.1"
|
||||
checksum: 10/8befa58f769b19d4a72c214694906b83b584310575300e63c08c48f9f2cfa6cb57fb4e1d08325961938d9dde3ecc4f5737b1604ddedfd759f5a1e65e5b0ca577
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-uri-to-path@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "file-uri-to-path@npm:1.0.0"
|
||||
@@ -32384,15 +32372,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-web-to-node-stream@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "readable-web-to-node-stream@npm:3.0.2"
|
||||
dependencies:
|
||||
readable-stream: "npm:^3.6.0"
|
||||
checksum: 10/d3a5bf9d707c01183d546a64864aa63df4d9cb835dfd2bf89ac8305e17389feef2170c4c14415a10d38f9b9bfddf829a57aaef7c53c8b40f11d499844bf8f1a4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readdirp@npm:~3.6.0":
|
||||
version: 3.6.0
|
||||
resolution: "readdirp@npm:3.6.0"
|
||||
@@ -34681,16 +34660,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strtok3@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "strtok3@npm:7.0.0"
|
||||
dependencies:
|
||||
"@tokenizer/token": "npm:^0.3.0"
|
||||
peek-readable: "npm:^5.0.0"
|
||||
checksum: 10/4f2269679fcfce1e9fe0600eff361ea4c687ae0a0e8d9dab6703811071cd92545cbcb32d4ace3d3aa591f777cec1a3e8aeecd5efd71ae216fd2962a7a238b4ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"style-loader@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "style-loader@npm:4.0.0"
|
||||
@@ -35327,16 +35296,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"token-types@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "token-types@npm:5.0.1"
|
||||
dependencies:
|
||||
"@tokenizer/token": "npm:^0.3.0"
|
||||
ieee754: "npm:^1.2.1"
|
||||
checksum: 10/0985369bbea9f53a5ccd79bb9899717b41401a813deb2c7fb1add5d0baf2f702aaf6da78f6e0ccf346d5a9f7acaa7cb5efed7d092d89d8c1e6962959e9509bc0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"toml@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "toml@npm:3.0.0"
|
||||
@@ -37672,7 +37631,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "y-provider@workspace:packages/common/y-provider"
|
||||
dependencies:
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404260628-ddb1941"
|
||||
"@blocksuite/store": "npm:0.14.0-canary-202404280529-c8e5f89"
|
||||
vite: "npm:^5.1.4"
|
||||
vite-plugin-dts: "npm:3.7.3"
|
||||
vitest: "npm:1.4.0"
|
||||
|
||||
Reference in New Issue
Block a user