mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 10:10:42 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dc69a3bef | |||
| e8d774a2ad |
@@ -27,11 +27,26 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
optimize_ci:
|
||||
name: Optimize CI
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.check_skip.outputs.skip }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Graphite CI Optimizer
|
||||
uses: withgraphite/graphite-ci-action@main
|
||||
id: check_skip
|
||||
with:
|
||||
graphite_token: ${{ secrets.GRAPHITE_CI_OPTIMIZER_TOKEN }}
|
||||
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=14384
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@@ -65,6 +80,9 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run oxlint
|
||||
@@ -90,6 +108,8 @@ jobs:
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=14384
|
||||
steps:
|
||||
@@ -117,6 +137,8 @@ jobs:
|
||||
lint-rust:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/build-rust
|
||||
@@ -137,7 +159,9 @@ jobs:
|
||||
name: Check Git Status
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
- build-server-native
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
@@ -173,6 +197,8 @@ jobs:
|
||||
check-yarn-binary:
|
||||
name: Check yarn binary
|
||||
runs-on: ubuntu-latest
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run check
|
||||
@@ -183,6 +209,8 @@ jobs:
|
||||
e2e-blocksuite-test:
|
||||
name: E2E BlockSuite Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -214,6 +242,8 @@ jobs:
|
||||
e2e-blocksuite-cross-browser-test:
|
||||
name: E2E BlockSuite Cross Browser Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -248,6 +278,8 @@ jobs:
|
||||
e2e-test:
|
||||
name: E2E Test
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
DISTRIBUTION: web
|
||||
IN_CI_TEST: true
|
||||
@@ -280,6 +312,8 @@ jobs:
|
||||
e2e-mobile-test:
|
||||
name: E2E Mobile Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
DISTRIBUTION: mobile
|
||||
IN_CI_TEST: true
|
||||
@@ -311,7 +345,9 @@ jobs:
|
||||
name: Unit Test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
- build-native
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
DISTRIBUTION: web
|
||||
strategy:
|
||||
@@ -348,6 +384,8 @@ jobs:
|
||||
build-native:
|
||||
name: Build AFFiNE native (${{ matrix.spec.target }})
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_DEBUG: '1'
|
||||
strategy:
|
||||
@@ -390,6 +428,8 @@ jobs:
|
||||
build-windows-native:
|
||||
name: Build AFFiNE native (${{ matrix.spec.target }})
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_DEBUG: '1'
|
||||
strategy:
|
||||
@@ -437,6 +477,8 @@ jobs:
|
||||
build-server-native:
|
||||
name: Build Server native
|
||||
runs-on: ubuntu-latest
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_DEBUG: '1'
|
||||
steps:
|
||||
@@ -462,6 +504,8 @@ jobs:
|
||||
build-electron-renderer:
|
||||
name: Build @affine/electron renderer
|
||||
runs-on: ubuntu-latest
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
@@ -487,7 +531,9 @@ jobs:
|
||||
name: Native Unit Test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
- build-native
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
@@ -507,7 +553,9 @@ jobs:
|
||||
name: Server Test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
- build-server-native
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -539,7 +587,7 @@ jobs:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
indexer:
|
||||
image: manticoresearch/manticore:10.1.0
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
@@ -580,7 +628,9 @@ jobs:
|
||||
name: Server Test with Elasticsearch
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
- build-server-native
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
env:
|
||||
@@ -663,7 +713,9 @@ jobs:
|
||||
name: Server E2E Test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
- build-server-native
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
@@ -685,7 +737,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
indexer:
|
||||
image: manticoresearch/manticore:10.1.0
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
@@ -721,6 +773,9 @@ jobs:
|
||||
miri:
|
||||
name: miri code check
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -734,9 +789,7 @@ jobs:
|
||||
toolchain: nightly
|
||||
components: miri
|
||||
- name: Install latest nextest release
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: nextest@0.9.98
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Miri Code Check
|
||||
continue-on-error: true
|
||||
@@ -746,6 +799,9 @@ jobs:
|
||||
loom:
|
||||
name: loom thread test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
RUSTFLAGS: --cfg loom
|
||||
RUST_BACKTRACE: full
|
||||
@@ -758,9 +814,7 @@ jobs:
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Install latest nextest release
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: nextest@0.9.98
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Loom Thread Test
|
||||
run: |
|
||||
@@ -769,6 +823,9 @@ jobs:
|
||||
fuzzing:
|
||||
name: fuzzing
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
@@ -815,6 +872,9 @@ jobs:
|
||||
- { target: 'aarch64-apple-darwin', os: 'macos-latest' }
|
||||
- { target: 'x86_64-pc-windows-msvc', os: 'windows-latest' }
|
||||
- { target: 'aarch64-pc-windows-msvc', os: 'windows-11-arm' }
|
||||
needs:
|
||||
- optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
@@ -848,6 +908,8 @@ jobs:
|
||||
rust-test:
|
||||
name: Run native tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
@@ -859,9 +921,7 @@ jobs:
|
||||
no-build: 'true'
|
||||
|
||||
- name: Install latest nextest release
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: nextest@0.9.98
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run tests
|
||||
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
|
||||
@@ -870,7 +930,9 @@ jobs:
|
||||
name: Server Copilot Api Test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
- build-server-native
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
NODE_ENV: test
|
||||
DISTRIBUTION: web
|
||||
@@ -898,7 +960,7 @@ jobs:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
indexer:
|
||||
image: manticoresearch/manticore:10.1.0
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
@@ -992,7 +1054,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
indexer:
|
||||
image: manticoresearch/manticore:10.1.0
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
@@ -1052,8 +1114,10 @@ jobs:
|
||||
name: ${{ matrix.tests.name }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
- build-server-native
|
||||
- build-native
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
DISTRIBUTION: web
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
@@ -1112,7 +1176,7 @@ jobs:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
indexer:
|
||||
image: manticoresearch/manticore:10.1.0
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
@@ -1157,8 +1221,10 @@ jobs:
|
||||
name: Desktop Test (${{ matrix.spec.os }}, ${{ matrix.spec.platform }}, ${{ matrix.spec.arch }}, ${{ matrix.spec.target }}, ${{ matrix.spec.test }})
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
needs:
|
||||
- optimize_ci
|
||||
- build-electron-renderer
|
||||
- build-native
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -1253,8 +1319,10 @@ jobs:
|
||||
name: Desktop bundle check (${{ matrix.spec.os }}, ${{ matrix.spec.platform }}, ${{ matrix.spec.arch }}, ${{ matrix.spec.target }}, ${{ matrix.spec.test }})
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
needs:
|
||||
- optimize_ci
|
||||
- build-electron-renderer
|
||||
- build-native
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -1360,6 +1428,8 @@ jobs:
|
||||
|
||||
test-build-mobile-app:
|
||||
uses: ./.github/workflows/release-mobile.yml
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
with:
|
||||
build-type: canary
|
||||
build-target: development
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
indexer:
|
||||
image: manticoresearch/manticore:10.1.0
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
indexer:
|
||||
image: manticoresearch/manticore:10.1.0
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
|
||||
Generated
+14
-318
@@ -77,10 +77,8 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"core-foundation",
|
||||
"coreaudio-rs 0.12.1",
|
||||
"cpal",
|
||||
"coreaudio-rs",
|
||||
"criterion2",
|
||||
"crossbeam-channel",
|
||||
"dispatch2",
|
||||
"libc",
|
||||
"napi",
|
||||
@@ -93,8 +91,6 @@ dependencies = [
|
||||
"symphonia",
|
||||
"thiserror 2.0.12",
|
||||
"uuid",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -217,28 +213,6 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
@@ -721,17 +695,9 @@ version = "1.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
@@ -887,16 +853,6 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -1023,17 +979,6 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.12.1"
|
||||
@@ -1054,29 +999,6 @@ dependencies = [
|
||||
"bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-rs 0.11.3",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"oboe",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows 0.54.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -1163,15 +1085,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
@@ -1252,12 +1165,6 @@ dependencies = [
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -1913,7 +1820,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.57.0",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2175,38 +2082,6 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
@@ -2284,7 +2159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2389,15 +2264,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
@@ -2504,9 +2370,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "3.0.0-beta.8"
|
||||
version = "3.0.0-beta.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c502f122fc89e92c6222810b3144411c6f945da5aa3b713ddfad3bdcae7c9bb4"
|
||||
checksum = "2a5c343e6e1fb57bf3ea3386638c4affb394ee932708128840a56aaac3d6a8ab"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
@@ -2514,23 +2380,21 @@ dependencies = [
|
||||
"ctor",
|
||||
"napi-build",
|
||||
"napi-sys",
|
||||
"nohash-hasher",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.2.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44e0e3177307063d3e7e55b7dd7b648cca9d7f46daa35422c0d98cc2bf48c2c1"
|
||||
checksum = "03acbfa4f156a32188bfa09b86dc11a431b5725253fc1fc6f6df5bed273382c4"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "3.0.0-beta.8"
|
||||
version = "3.0.0-beta.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcf1e732a67e934b069d6d527251d6288753a36840572abe132a7aed9e77f0bc"
|
||||
checksum = "08d23065ee795a4b1a8755fdf4a39c2a229679f01f923a8feea33f045d6d96cb"
|
||||
dependencies = [
|
||||
"convert_case 0.8.0",
|
||||
"ctor",
|
||||
@@ -2542,9 +2406,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "2.0.0-beta.8"
|
||||
version = "2.0.0-beta.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "462b775ba74791c98989fadc46c4bb2ec53016427be4d420d31c4bbaab34b308"
|
||||
checksum = "348aaac2c51b5d11cf90cf7670b470c7f4d1607d15c338efd4d3db361003e4f5"
|
||||
dependencies = [
|
||||
"convert_case 0.8.0",
|
||||
"proc-macro2",
|
||||
@@ -2555,42 +2419,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "3.0.0-alpha.3"
|
||||
version = "3.0.0-alpha.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4401c63f866b42d673a8b213d5662c84a0701b0f6c3acff7e2b9fc439f1675d"
|
||||
checksum = "b443b980b2258dbaa31b99115e74da6c0866e537278309d566b4672a2f8df516"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.5.0+25.2.9519653"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
|
||||
dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
@@ -2609,12 +2444,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -2681,17 +2510,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
@@ -2810,29 +2628,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oboe"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
|
||||
dependencies = [
|
||||
"jni",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"oboe-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oboe-sys"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -5208,19 +5003,6 @@ dependencies = [
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.100"
|
||||
@@ -5334,7 +5116,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5343,16 +5125,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||
dependencies = [
|
||||
"windows-core 0.54.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.57.0"
|
||||
@@ -5385,16 +5157,6 @@ dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||
dependencies = [
|
||||
"windows-result 0.1.2",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.57.0"
|
||||
@@ -5518,15 +5280,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
@@ -5554,21 +5307,6 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
@@ -5609,12 +5347,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
@@ -5627,12 +5359,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
@@ -5645,12 +5371,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
@@ -5669,12 +5389,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
@@ -5687,12 +5401,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
@@ -5705,12 +5413,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
@@ -5723,12 +5425,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
|
||||
-15
@@ -28,14 +28,12 @@ base64-simd = "0.8"
|
||||
bitvec = "1.0"
|
||||
block2 = "0.6"
|
||||
byteorder = "1.5"
|
||||
cpal = "0.15"
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
core-foundation = "0.10"
|
||||
coreaudio-rs = "0.12"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
criterion2 = { version = "3", default-features = false }
|
||||
crossbeam-channel = "0.5"
|
||||
dispatch2 = "0.3"
|
||||
docx-parser = { git = "https://github.com/toeverything/docx-parser" }
|
||||
dotenvy = "0.15"
|
||||
@@ -99,19 +97,6 @@ uniffi = "0.29"
|
||||
url = { version = "2.5" }
|
||||
uuid = "1.8"
|
||||
v_htmlescape = "0.15"
|
||||
windows = { version = "0.61", features = [
|
||||
"Win32_Devices_FunctionDiscovery",
|
||||
"Win32_UI_Shell_PropertiesSystem",
|
||||
"Win32_Media_Audio",
|
||||
"Win32_System_Variant",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_ProcessStatus",
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
] }
|
||||
windows-core = { version = "0.61" }
|
||||
y-octo = { path = "./packages/common/y-octo/core" }
|
||||
y-sync = { version = "0.4" }
|
||||
yrs = "0.23.0"
|
||||
|
||||
@@ -62,11 +62,13 @@ const builtinSurfaceToolbarConfig = {
|
||||
if (!rootModel) return;
|
||||
|
||||
const { id: frameId, xywh } = model;
|
||||
let lastNoteId = rootModel.children.findLast(
|
||||
note =>
|
||||
matchModels(note, [NoteBlockModel]) &&
|
||||
note.props.displayMode !== NoteDisplayMode.EdgelessOnly
|
||||
)?.id;
|
||||
let lastNoteId = rootModel.children
|
||||
.filter(
|
||||
note =>
|
||||
matchModels(note, [NoteBlockModel]) &&
|
||||
note.props.displayMode !== NoteDisplayMode.EdgelessOnly
|
||||
)
|
||||
.pop()?.id;
|
||||
|
||||
if (!lastNoteId) {
|
||||
const bounds = Bound.deserialize(xywh);
|
||||
|
||||
@@ -14,7 +14,6 @@ export class EdgelessNoteMask extends SignalWatcher(
|
||||
protected override firstUpdated() {
|
||||
const maskDOM = this.renderRoot!.querySelector('.affine-note-mask');
|
||||
const observer = new ResizeObserver(entries => {
|
||||
if (this.model.store.readonly) return;
|
||||
for (const entry of entries) {
|
||||
if (!this.model.props.edgeless.collapse) {
|
||||
const bound = Bound.deserialize(this.model.xywh);
|
||||
|
||||
@@ -28,6 +28,7 @@ import { query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { ParagraphBlockConfigExtension } from './paragraph-block-config.js';
|
||||
import { paragraphBlockStyles } from './styles.js';
|
||||
@@ -227,6 +228,12 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
}
|
||||
|
||||
override renderBlock(): TemplateResult<1> {
|
||||
const widgets = html`${repeat(
|
||||
Object.entries(this.widgets),
|
||||
([id]) => id,
|
||||
([_, widget]) => widget
|
||||
)}`;
|
||||
|
||||
const { type$ } = this.model.props;
|
||||
const collapsed = this.store.readonly
|
||||
? this._readonlyCollapsed
|
||||
@@ -341,6 +348,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
</div>
|
||||
|
||||
${children}
|
||||
${widgets}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import type { ClassInfo } from 'lit-html/directives/class-map.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
|
||||
import { MenuFocusable } from './focusable.js';
|
||||
import type { Menu } from './menu.js';
|
||||
@@ -22,7 +21,6 @@ export type MenuButtonData = {
|
||||
class: ClassInfo;
|
||||
select: (ele: HTMLElement) => void | false;
|
||||
onHover?: (hover: boolean) => void;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
export class MenuButton extends MenuFocusable {
|
||||
@@ -99,12 +97,7 @@ export class MenuButton extends MenuFocusable {
|
||||
focused: this.isFocused$.value,
|
||||
...this.data.class,
|
||||
});
|
||||
return html` <div
|
||||
class="${classString}"
|
||||
data-testid=${ifDefined(this.data.testId)}
|
||||
>
|
||||
${this.data.content()}
|
||||
</div>`;
|
||||
return html` <div class="${classString}">${this.data.content()}</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -164,12 +157,7 @@ export class MobileMenuButton extends MenuFocusable {
|
||||
focused: this.isFocused$.value,
|
||||
...this.data.class,
|
||||
});
|
||||
return html` <div
|
||||
class="${classString}"
|
||||
data-testid=${ifDefined(this.data.testId)}
|
||||
>
|
||||
${this.data.content()}
|
||||
</div>`;
|
||||
return html` <div class="${classString}">${this.data.content()}</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -200,7 +188,6 @@ export const menuButtonItems = {
|
||||
onHover?: (hover: boolean) => void;
|
||||
class?: MenuClass;
|
||||
hide?: () => boolean;
|
||||
testId?: string;
|
||||
}) =>
|
||||
menu => {
|
||||
if (config.hide?.() || !menu.search(config.name)) {
|
||||
@@ -222,7 +209,6 @@ export const menuButtonItems = {
|
||||
'selected-item': config.isSelected ?? false,
|
||||
...config.class,
|
||||
},
|
||||
testId: config.testId,
|
||||
};
|
||||
return renderButton(data, menu);
|
||||
},
|
||||
@@ -234,7 +220,6 @@ export const menuButtonItems = {
|
||||
label?: () => TemplateResult;
|
||||
select: (checked: boolean) => boolean;
|
||||
class?: ClassInfo;
|
||||
testId?: string;
|
||||
}) =>
|
||||
menu => {
|
||||
if (!menu.search(config.name)) {
|
||||
@@ -255,7 +240,6 @@ export const menuButtonItems = {
|
||||
return false;
|
||||
},
|
||||
class: config.class ?? {},
|
||||
testId: config.testId,
|
||||
};
|
||||
return html`${keyed(config.name, renderButton(data, menu))}`;
|
||||
},
|
||||
@@ -263,12 +247,10 @@ export const menuButtonItems = {
|
||||
(config: {
|
||||
name: string;
|
||||
on: boolean;
|
||||
prefix?: TemplateResult;
|
||||
postfix?: TemplateResult;
|
||||
label?: () => TemplateResult;
|
||||
onChange: (on: boolean) => void;
|
||||
class?: ClassInfo;
|
||||
testId?: string;
|
||||
}) =>
|
||||
menu => {
|
||||
if (!menu.search(config.name)) {
|
||||
@@ -280,7 +262,6 @@ export const menuButtonItems = {
|
||||
|
||||
const data: MenuButtonData = {
|
||||
content: () => html`
|
||||
${config.prefix}
|
||||
<div class="affine-menu-action-text">
|
||||
${config.label?.() ?? config.name}
|
||||
</div>
|
||||
@@ -295,7 +276,6 @@ export const menuButtonItems = {
|
||||
return false;
|
||||
},
|
||||
class: config.class ?? {},
|
||||
testId: config.testId,
|
||||
};
|
||||
return html`${keyed(config.name, renderButton(data, menu))}`;
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@ export type MenuOptions = {
|
||||
placeholder?: string;
|
||||
};
|
||||
items: MenuConfig[];
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
// Global menu open listener type
|
||||
@@ -73,9 +72,6 @@ export class Menu {
|
||||
? document.createElement('mobile-menu')
|
||||
: document.createElement('affine-menu');
|
||||
this.menuElement.menu = this;
|
||||
if (this.options.testId) {
|
||||
this.menuElement.dataset.testid = this.options.testId;
|
||||
}
|
||||
|
||||
// Call global menu open listeners
|
||||
menuOpenListeners.forEach(listener => {
|
||||
|
||||
@@ -111,7 +111,6 @@ export class FilterableListComponent<Props = unknown> extends WithDisposable(
|
||||
if (ev.isComposing) break;
|
||||
ev.preventDefault();
|
||||
const item = filteredItems[this._curFocusIndex];
|
||||
if (!item) return;
|
||||
this._select(item);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const selectPropertyModelConfig = selectPropertyType.modelConfig({
|
||||
const name = oldValue
|
||||
.split(',')
|
||||
.map(v => v.trim())
|
||||
.find(v => v);
|
||||
.filter(v => v)[0];
|
||||
if (!name) {
|
||||
return { value: null, data: data };
|
||||
}
|
||||
|
||||
@@ -40,11 +40,13 @@ export const groupToolbarConfig = {
|
||||
if (!rootModel) return;
|
||||
|
||||
const { id: groupId, xywh } = model;
|
||||
let lastNoteId = rootModel.children.findLast(
|
||||
note =>
|
||||
matchModels(note, [NoteBlockModel]) &&
|
||||
note.props.displayMode !== NoteDisplayMode.EdgelessOnly
|
||||
)?.id;
|
||||
let lastNoteId = rootModel.children
|
||||
.filter(
|
||||
note =>
|
||||
matchModels(note, [NoteBlockModel]) &&
|
||||
note.props.displayMode !== NoteDisplayMode.EdgelessOnly
|
||||
)
|
||||
.pop()?.id;
|
||||
|
||||
if (!lastNoteId) {
|
||||
const bounds = Bound.deserialize(xywh);
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/icons": "^2.2.12",
|
||||
@@ -63,7 +65,8 @@
|
||||
"./theme": "./src/theme/index.ts",
|
||||
"./styles": "./src/styles/index.ts",
|
||||
"./services": "./src/services/index.ts",
|
||||
"./adapters": "./src/adapters/index.ts"
|
||||
"./adapters": "./src/adapters/index.ts",
|
||||
"./test-utils": "./src/test-utils/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getFirstBlockCommand } from '../../../commands/block-crud/get-first-content-block';
|
||||
import { affine } from '../../helpers/affine-template';
|
||||
import { affine } from '../../../test-utils';
|
||||
|
||||
describe('commands/block-crud', () => {
|
||||
describe('getFirstBlockCommand', () => {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getLastBlockCommand } from '../../../commands/block-crud/get-last-content-block';
|
||||
import { affine } from '../../helpers/affine-template';
|
||||
import { affine } from '../../../test-utils';
|
||||
|
||||
describe('commands/block-crud', () => {
|
||||
describe('getLastBlockCommand', () => {
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import '../../helpers/affine-test-utils';
|
||||
import '../../../test-utils/affine-test-utils';
|
||||
|
||||
import type { TextSelection } from '@blocksuite/std';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { replaceSelectedTextWithBlocksCommand } from '../../../commands/model-crud/replace-selected-text-with-blocks';
|
||||
import { affine, block } from '../../helpers/affine-template';
|
||||
import { affine, block } from '../../../test-utils';
|
||||
|
||||
describe('commands/model-crud', () => {
|
||||
describe('replaceSelectedTextWithBlocksCommand', () => {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { isNothingSelectedCommand } from '../../../commands/selection/is-nothing-selected';
|
||||
import { ImageSelection } from '../../../selection';
|
||||
import { affine } from '../../helpers/affine-template';
|
||||
import { affine } from '../../../test-utils';
|
||||
|
||||
describe('commands/selection', () => {
|
||||
describe('isNothingSelectedCommand', () => {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { TextSelection } from '@blocksuite/std';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { affine } from './affine-template';
|
||||
import { affine } from '../../test-utils';
|
||||
|
||||
describe('helpers/affine-template', () => {
|
||||
it('should create a basic document structure from template', () => {
|
||||
@@ -3,7 +3,7 @@ import { SurfaceSelection } from '@blocksuite/std';
|
||||
import type { GetSelectionCommand } from './types';
|
||||
|
||||
export const getSurfaceSelectionCommand: GetSelectionCommand = (ctx, next) => {
|
||||
const currentSurfaceSelection = ctx.std.selection.find(SurfaceSelection);
|
||||
const currentSurfaceSelection = ctx.std.selection.filter(SurfaceSelection)[0];
|
||||
if (!currentSurfaceSelection) return;
|
||||
|
||||
next({ currentSurfaceSelection });
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface BlockSuiteFlags {
|
||||
enable_table_virtual_scroll: boolean;
|
||||
enable_turbo_renderer: boolean;
|
||||
enable_dom_renderer: boolean;
|
||||
enable_web_container: boolean;
|
||||
}
|
||||
|
||||
export class FeatureFlagService extends StoreExtension {
|
||||
@@ -47,7 +46,6 @@ export class FeatureFlagService extends StoreExtension {
|
||||
enable_table_virtual_scroll: false,
|
||||
enable_turbo_renderer: false,
|
||||
enable_dom_renderer: false,
|
||||
enable_web_container: false,
|
||||
});
|
||||
|
||||
setFlag(key: keyof BlockSuiteFlags, value: boolean) {
|
||||
|
||||
+29
-23
@@ -1,29 +1,32 @@
|
||||
import {
|
||||
CodeBlockSchemaExtension,
|
||||
DatabaseBlockSchemaExtension,
|
||||
ImageBlockSchemaExtension,
|
||||
ListBlockSchemaExtension,
|
||||
NoteBlockSchemaExtension,
|
||||
ParagraphBlockSchemaExtension,
|
||||
RootBlockSchemaExtension,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { getInternalStoreExtensions } from '@blocksuite/affine/extensions/store';
|
||||
import { StoreExtensionManager } from '@blocksuite/affine-ext-loader';
|
||||
import { Container } from '@blocksuite/global/di';
|
||||
import { TextSelection } from '@blocksuite/std';
|
||||
import { type Block, type Store } from '@blocksuite/store';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import { type Block, type Store, Text } from '@blocksuite/store';
|
||||
import { TestWorkspace } from '@blocksuite/store/test';
|
||||
|
||||
import { createTestHost } from './create-test-host';
|
||||
|
||||
// Extensions array
|
||||
const extensions = [
|
||||
RootBlockSchemaExtension,
|
||||
NoteBlockSchemaExtension,
|
||||
ParagraphBlockSchemaExtension,
|
||||
ListBlockSchemaExtension,
|
||||
ImageBlockSchemaExtension,
|
||||
DatabaseBlockSchemaExtension,
|
||||
CodeBlockSchemaExtension,
|
||||
];
|
||||
const manager = new StoreExtensionManager(getInternalStoreExtensions());
|
||||
const extensions = manager.get('store');
|
||||
|
||||
// // Extensions array
|
||||
// const extensions = [
|
||||
// RootBlockSchemaExtension,
|
||||
// NoteBlockSchemaExtension,
|
||||
// ParagraphBlockSchemaExtension,
|
||||
// ListBlockSchemaExtension,
|
||||
// ImageBlockSchemaExtension,
|
||||
// DatabaseBlockSchemaExtension,
|
||||
// CodeBlockSchemaExtension,
|
||||
// RootStoreExtension,
|
||||
// NoteStoreExtension,
|
||||
// ParagraphStoreExtension,
|
||||
// ListStoreExtension,
|
||||
// ImageStoreExtension,
|
||||
// DatabaseStoreExtension,
|
||||
// CodeStoreExtension
|
||||
// ];
|
||||
|
||||
// Mapping from tag names to flavours
|
||||
const tagToFlavour: Record<string, string> = {
|
||||
@@ -75,8 +78,11 @@ export function affine(strings: TemplateStringsArray, ...values: any[]) {
|
||||
const workspace = new TestWorkspace({});
|
||||
workspace.meta.initialize();
|
||||
const doc = workspace.createDoc('test-doc');
|
||||
const store = doc.getStore({ extensions });
|
||||
|
||||
const container = new Container();
|
||||
extensions.forEach(extension => {
|
||||
extension.setup(container);
|
||||
});
|
||||
const store = doc.getStore({ extensions, provider: container.provider() });
|
||||
let selectionInfo: SelectionInfo = {};
|
||||
|
||||
// Use DOMParser to parse HTML string
|
||||
+2
-4
@@ -63,10 +63,8 @@ function compareBlocks(
|
||||
if (JSON.stringify(actualProps) !== JSON.stringify(expectedProps))
|
||||
return false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||||
for (let i = 0; i < actual.children.length; i++) {
|
||||
if (!compareBlocks(actual.children[i], expected.children[i], compareId))
|
||||
return false;
|
||||
for (const [i, child] of actual.children.entries()) {
|
||||
if (!compareBlocks(child, expected.children[i], compareId)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
+1
-1
@@ -240,7 +240,7 @@ export function createTestHost(doc: Store): EditorHost {
|
||||
std.selection = new MockSelectionStore();
|
||||
|
||||
std.command = new CommandManager(std as any);
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error dev-only
|
||||
host.command = std.command;
|
||||
host.selection = std.selection;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './affine-template';
|
||||
export * from './affine-test-utils';
|
||||
export * from './create-test-host';
|
||||
@@ -20,7 +20,3 @@
|
||||
|
||||
- [Store](classes/Store.md)
|
||||
- [StoreSlots](interfaces/StoreSlots.md)
|
||||
|
||||
## Other
|
||||
|
||||
- [Schema](classes/Schema.md)
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
[**BlockSuite API Documentation**](../../../README.md)
|
||||
|
||||
***
|
||||
|
||||
[BlockSuite API Documentation](../../../README.md) / [@blocksuite/store](../README.md) / Schema
|
||||
|
||||
# Class: Schema
|
||||
|
||||
Represents a schema manager for block flavours and their relationships.
|
||||
Provides methods to register, validate, and query block schemas.
|
||||
|
||||
## Properties
|
||||
|
||||
### flavourSchemaMap
|
||||
|
||||
> `readonly` **flavourSchemaMap**: `Map`\<`string`, \{ `model`: \{ `children?`: `string`[]; `flavour`: `string`; `isFlatData?`: `boolean`; `parent?`: `string`[]; `props?`: (...`args`) => `Record`\<`string`, `any`\>; `role`: `string`; `toModel?`: (...`args`) => `BlockModel`\<`object`\>; \}; `transformer?`: (...`args`) => `BaseBlockTransformer`\<`object`\>; `version`: `number`; \}\>
|
||||
|
||||
A map storing block flavour names to their corresponding schema definitions.
|
||||
|
||||
## Accessors
|
||||
|
||||
### versions
|
||||
|
||||
#### Get Signature
|
||||
|
||||
> **get** **versions**(): `object`
|
||||
|
||||
Returns an object mapping each registered flavour to its version number.
|
||||
|
||||
##### Returns
|
||||
|
||||
`object`
|
||||
|
||||
## Methods
|
||||
|
||||
### get()
|
||||
|
||||
> **get**(`flavour`): `undefined` \| \{ `model`: \{ `children?`: `string`[]; `flavour`: `string`; `isFlatData?`: `boolean`; `parent?`: `string`[]; `props?`: (...`args`) => `Record`\<`string`, `any`\>; `role`: `string`; `toModel?`: (...`args`) => `BlockModel`\<`object`\>; \}; `transformer?`: (...`args`) => `BaseBlockTransformer`\<`object`\>; `version`: `number`; \}
|
||||
|
||||
Retrieves the schema for a given block flavour.
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### flavour
|
||||
|
||||
`string`
|
||||
|
||||
The block flavour name.
|
||||
|
||||
#### Returns
|
||||
|
||||
`undefined` \| \{ `model`: \{ `children?`: `string`[]; `flavour`: `string`; `isFlatData?`: `boolean`; `parent?`: `string`[]; `props?`: (...`args`) => `Record`\<`string`, `any`\>; `role`: `string`; `toModel?`: (...`args`) => `BlockModel`\<`object`\>; \}; `transformer?`: (...`args`) => `BaseBlockTransformer`\<`object`\>; `version`: `number`; \}
|
||||
|
||||
The corresponding BlockSchemaType or undefined if not found.
|
||||
|
||||
***
|
||||
|
||||
### isValid()
|
||||
|
||||
> **isValid**(`child`, `parent`): `boolean`
|
||||
|
||||
Checks if the child flavour is valid under the parent flavour.
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### child
|
||||
|
||||
`string`
|
||||
|
||||
The child block flavour name.
|
||||
|
||||
##### parent
|
||||
|
||||
`string`
|
||||
|
||||
The parent block flavour name.
|
||||
|
||||
#### Returns
|
||||
|
||||
`boolean`
|
||||
|
||||
True if the relationship is valid, false otherwise.
|
||||
|
||||
***
|
||||
|
||||
### register()
|
||||
|
||||
> **register**(`blockSchema`): `Schema`
|
||||
|
||||
Registers an array of block schemas into the schema manager.
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### blockSchema
|
||||
|
||||
`object`[]
|
||||
|
||||
An array of block schema definitions to register.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Schema`
|
||||
|
||||
The Schema instance (for chaining).
|
||||
|
||||
***
|
||||
|
||||
### safeValidate()
|
||||
|
||||
> **safeValidate**(`flavour`, `parentFlavour?`, `childFlavours?`): `boolean`
|
||||
|
||||
Safely validates the schema relationship for a given flavour, parent, and children.
|
||||
Returns true if valid, false otherwise (does not throw).
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### flavour
|
||||
|
||||
`string`
|
||||
|
||||
The block flavour to validate.
|
||||
|
||||
##### parentFlavour?
|
||||
|
||||
`string`
|
||||
|
||||
The parent block flavour (optional).
|
||||
|
||||
##### childFlavours?
|
||||
|
||||
`string`[]
|
||||
|
||||
The child block flavours (optional).
|
||||
|
||||
#### Returns
|
||||
|
||||
`boolean`
|
||||
|
||||
True if the schema relationship is valid, false otherwise.
|
||||
|
||||
***
|
||||
|
||||
### toJSON()
|
||||
|
||||
> **toJSON**(): `object`
|
||||
|
||||
Serializes the schema map to a plain object for JSON output.
|
||||
|
||||
#### Returns
|
||||
|
||||
`object`
|
||||
|
||||
An object mapping each flavour to its role, parent, and children.
|
||||
|
||||
***
|
||||
|
||||
### validate()
|
||||
|
||||
> **validate**(`flavour`, `parentFlavour?`, `childFlavours?`): `void`
|
||||
|
||||
Validates the schema relationship for a given flavour, parent, and children.
|
||||
Throws SchemaValidateError if invalid.
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### flavour
|
||||
|
||||
`string`
|
||||
|
||||
The block flavour to validate.
|
||||
|
||||
##### parentFlavour?
|
||||
|
||||
`string`
|
||||
|
||||
The parent block flavour (optional).
|
||||
|
||||
##### childFlavours?
|
||||
|
||||
`string`[]
|
||||
|
||||
The child block flavours (optional).
|
||||
|
||||
#### Returns
|
||||
|
||||
`void`
|
||||
|
||||
#### Throws
|
||||
|
||||
If the schema relationship is invalid.
|
||||
|
||||
***
|
||||
|
||||
### validateSchema()
|
||||
|
||||
> **validateSchema**(`child`, `parent`): `void`
|
||||
|
||||
Validates the relationship between a child and parent schema.
|
||||
Throws if the relationship is invalid.
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### child
|
||||
|
||||
The child block schema.
|
||||
|
||||
###### model
|
||||
|
||||
\{ `children?`: `string`[]; `flavour`: `string`; `isFlatData?`: `boolean`; `parent?`: `string`[]; `props?`: (...`args`) => `Record`\<`string`, `any`\>; `role`: `string`; `toModel?`: (...`args`) => `BlockModel`\<`object`\>; \} = `...`
|
||||
|
||||
###### model.children?
|
||||
|
||||
`string`[] = `ContentSchema`
|
||||
|
||||
###### model.flavour
|
||||
|
||||
`string` = `FlavourSchema`
|
||||
|
||||
###### model.isFlatData?
|
||||
|
||||
`boolean` = `...`
|
||||
|
||||
###### model.parent?
|
||||
|
||||
`string`[] = `ParentSchema`
|
||||
|
||||
###### model.props?
|
||||
|
||||
(...`args`) => `Record`\<`string`, `any`\> = `...`
|
||||
|
||||
###### model.role
|
||||
|
||||
`string` = `RoleSchema`
|
||||
|
||||
###### model.toModel?
|
||||
|
||||
(...`args`) => `BlockModel`\<`object`\> = `...`
|
||||
|
||||
###### transformer?
|
||||
|
||||
(...`args`) => `BaseBlockTransformer`\<`object`\> = `...`
|
||||
|
||||
###### version
|
||||
|
||||
`number` = `...`
|
||||
|
||||
##### parent
|
||||
|
||||
The parent block schema.
|
||||
|
||||
###### model
|
||||
|
||||
\{ `children?`: `string`[]; `flavour`: `string`; `isFlatData?`: `boolean`; `parent?`: `string`[]; `props?`: (...`args`) => `Record`\<`string`, `any`\>; `role`: `string`; `toModel?`: (...`args`) => `BlockModel`\<`object`\>; \} = `...`
|
||||
|
||||
###### model.children?
|
||||
|
||||
`string`[] = `ContentSchema`
|
||||
|
||||
###### model.flavour
|
||||
|
||||
`string` = `FlavourSchema`
|
||||
|
||||
###### model.isFlatData?
|
||||
|
||||
`boolean` = `...`
|
||||
|
||||
###### model.parent?
|
||||
|
||||
`string`[] = `ParentSchema`
|
||||
|
||||
###### model.props?
|
||||
|
||||
(...`args`) => `Record`\<`string`, `any`\> = `...`
|
||||
|
||||
###### model.role
|
||||
|
||||
`string` = `RoleSchema`
|
||||
|
||||
###### model.toModel?
|
||||
|
||||
(...`args`) => `BlockModel`\<`object`\> = `...`
|
||||
|
||||
###### transformer?
|
||||
|
||||
(...`args`) => `BaseBlockTransformer`\<`object`\> = `...`
|
||||
|
||||
###### version
|
||||
|
||||
`number` = `...`
|
||||
|
||||
#### Returns
|
||||
|
||||
`void`
|
||||
|
||||
#### Throws
|
||||
|
||||
If the relationship is invalid.
|
||||
@@ -1035,13 +1035,13 @@ Get the Doc instance for current store.
|
||||
|
||||
#### Get Signature
|
||||
|
||||
> **get** **schema**(): [`Schema`](Schema.md)
|
||||
> **get** **schema**(): `Schema`
|
||||
|
||||
Get the [Schema](Schema.md) instance of the store.
|
||||
Get the Schema instance of the store.
|
||||
|
||||
##### Returns
|
||||
|
||||
[`Schema`](Schema.md)
|
||||
`Schema`
|
||||
|
||||
***
|
||||
|
||||
|
||||
@@ -4,25 +4,9 @@ import { SCHEMA_NOT_FOUND_MESSAGE } from '../consts.js';
|
||||
import { BlockSchema, type BlockSchemaType } from '../model/index.js';
|
||||
import { SchemaValidateError } from './error.js';
|
||||
|
||||
/**
|
||||
* Represents a schema manager for block flavours and their relationships.
|
||||
* Provides methods to register, validate, and query block schemas.
|
||||
*/
|
||||
export class Schema {
|
||||
/**
|
||||
* A map storing block flavour names to their corresponding schema definitions.
|
||||
*/
|
||||
readonly flavourSchemaMap = new Map<string, BlockSchemaType>();
|
||||
|
||||
/**
|
||||
* Safely validates the schema relationship for a given flavour, parent, and children.
|
||||
* Returns true if valid, false otherwise (does not throw).
|
||||
*
|
||||
* @param flavour - The block flavour to validate.
|
||||
* @param parentFlavour - The parent block flavour (optional).
|
||||
* @param childFlavours - The child block flavours (optional).
|
||||
* @returns True if the schema relationship is valid, false otherwise.
|
||||
*/
|
||||
safeValidate = (
|
||||
flavour: string,
|
||||
parentFlavour?: string,
|
||||
@@ -36,25 +20,10 @@ export class Schema {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the schema for a given block flavour.
|
||||
*
|
||||
* @param flavour - The block flavour name.
|
||||
* @returns The corresponding BlockSchemaType or undefined if not found.
|
||||
*/
|
||||
get(flavour: string) {
|
||||
return this.flavourSchemaMap.get(flavour);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the schema relationship for a given flavour, parent, and children.
|
||||
* Throws SchemaValidateError if invalid.
|
||||
*
|
||||
* @param flavour - The block flavour to validate.
|
||||
* @param parentFlavour - The parent block flavour (optional).
|
||||
* @param childFlavours - The child block flavours (optional).
|
||||
* @throws {SchemaValidateError} If the schema relationship is invalid.
|
||||
*/
|
||||
validate = (
|
||||
flavour: string,
|
||||
parentFlavour?: string,
|
||||
@@ -102,9 +71,6 @@ export class Schema {
|
||||
validateChildren();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an object mapping each registered flavour to its version number.
|
||||
*/
|
||||
get versions() {
|
||||
return Object.fromEntries(
|
||||
Array.from(this.flavourSchemaMap.values()).map(
|
||||
@@ -113,13 +79,6 @@ export class Schema {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two flavours match, using minimatch for wildcard support.
|
||||
*
|
||||
* @param childFlavour - The child block flavour.
|
||||
* @param parentFlavour - The parent block flavour.
|
||||
* @returns True if the flavours match, false otherwise.
|
||||
*/
|
||||
private _matchFlavour(childFlavour: string, parentFlavour: string) {
|
||||
return (
|
||||
minimatch(childFlavour, parentFlavour) ||
|
||||
@@ -127,15 +86,6 @@ export class Schema {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two values match as either flavours or roles, supporting role syntax (e.g., '@role').
|
||||
*
|
||||
* @param childValue - The child value (flavour or role).
|
||||
* @param parentValue - The parent value (flavour or role).
|
||||
* @param childRole - The actual role of the child.
|
||||
* @param parentRole - The actual role of the parent.
|
||||
* @returns True if the values match as flavours or roles, false otherwise.
|
||||
*/
|
||||
private _matchFlavourOrRole(
|
||||
childValue: string,
|
||||
parentValue: string,
|
||||
@@ -162,13 +112,6 @@ export class Schema {
|
||||
return this._matchFlavour(childValue, parentValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the parent schema is a valid parent for the child schema.
|
||||
*
|
||||
* @param child - The child block schema.
|
||||
* @param parent - The parent block schema.
|
||||
* @returns True if the parent is valid for the child, false otherwise.
|
||||
*/
|
||||
private _validateParent(
|
||||
child: BlockSchemaType,
|
||||
parent: BlockSchemaType
|
||||
@@ -226,14 +169,6 @@ export class Schema {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the role relationship between child and parent schemas.
|
||||
* Throws if the child is a root block but has a parent.
|
||||
*
|
||||
* @param child - The child block schema.
|
||||
* @param parent - The parent block schema.
|
||||
* @throws {SchemaValidateError} If the child is a root block with a parent.
|
||||
*/
|
||||
private _validateRole(child: BlockSchemaType, parent: BlockSchemaType) {
|
||||
const childRole = child.model.role;
|
||||
const childFlavour = child.model.flavour;
|
||||
@@ -247,13 +182,6 @@ export class Schema {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the child flavour is valid under the parent flavour.
|
||||
*
|
||||
* @param child - The child block flavour name.
|
||||
* @param parent - The parent block flavour name.
|
||||
* @returns True if the relationship is valid, false otherwise.
|
||||
*/
|
||||
isValid(child: string, parent: string) {
|
||||
const childSchema = this.flavourSchemaMap.get(child);
|
||||
const parentSchema = this.flavourSchemaMap.get(parent);
|
||||
@@ -268,12 +196,6 @@ export class Schema {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an array of block schemas into the schema manager.
|
||||
*
|
||||
* @param blockSchema - An array of block schema definitions to register.
|
||||
* @returns The Schema instance (for chaining).
|
||||
*/
|
||||
register(blockSchema: BlockSchemaType[]) {
|
||||
blockSchema.forEach(schema => {
|
||||
BlockSchema.parse(schema);
|
||||
@@ -282,11 +204,6 @@ export class Schema {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the schema map to a plain object for JSON output.
|
||||
*
|
||||
* @returns An object mapping each flavour to its role, parent, and children.
|
||||
*/
|
||||
toJSON() {
|
||||
return Object.fromEntries(
|
||||
Array.from(this.flavourSchemaMap.values()).map(
|
||||
@@ -302,14 +219,6 @@ export class Schema {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the relationship between a child and parent schema.
|
||||
* Throws if the relationship is invalid.
|
||||
*
|
||||
* @param child - The child block schema.
|
||||
* @param parent - The parent block schema.
|
||||
* @throws {SchemaValidateError} If the relationship is invalid.
|
||||
*/
|
||||
validateSchema(child: BlockSchemaType, parent: BlockSchemaType) {
|
||||
this._validateRole(child, parent);
|
||||
|
||||
|
||||
+2
-2
@@ -78,11 +78,11 @@
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-sonarjs": "^3.0.1",
|
||||
"eslint-plugin-unicorn": "^59.0.0",
|
||||
"happy-dom": "^18.0.0",
|
||||
"happy-dom": "^17.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.0.0",
|
||||
"msw": "^2.6.8",
|
||||
"oxlint": "^1.1.0",
|
||||
"oxlint": "0.16.11",
|
||||
"prettier": "^3.4.2",
|
||||
"semver": "^7.6.3",
|
||||
"serve": "^14.2.4",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"build:debug": "napi build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.0.0-alpha.89",
|
||||
"@napi-rs/cli": "3.0.0-alpha.81",
|
||||
"lib0": "^0.2.99",
|
||||
"tiktoken": "^1.0.17",
|
||||
"tinybench": "^4.0.0",
|
||||
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_sessions_messages" ADD COLUMN "streamObjects" JSON;
|
||||
@@ -147,7 +147,7 @@
|
||||
"c8": "^10.1.3",
|
||||
"nodemon": "^3.1.7",
|
||||
"react-email": "4.0.11",
|
||||
"sinon": "^21.0.0",
|
||||
"sinon": "^20.0.0",
|
||||
"supertest": "^7.0.0",
|
||||
"why-is-node-running": "^3.2.2"
|
||||
},
|
||||
|
||||
@@ -414,15 +414,14 @@ model AiPrompt {
|
||||
}
|
||||
|
||||
model AiSessionMessage {
|
||||
id String @id @default(uuid()) @db.VarChar
|
||||
sessionId String @map("session_id") @db.VarChar
|
||||
role AiPromptRole
|
||||
content String @db.Text
|
||||
streamObjects Json? @db.Json
|
||||
attachments Json? @db.Json
|
||||
params Json? @db.Json
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
id String @id @default(uuid()) @db.VarChar
|
||||
sessionId String @map("session_id") @db.VarChar
|
||||
role AiPromptRole
|
||||
content String @db.Text
|
||||
attachments Json? @db.Json
|
||||
params Json? @db.Json
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
session AiSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,5 @@
|
||||
import type { ExecutionContext, TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ServerFeature, ServerService } from '../core';
|
||||
import { AuthService } from '../core/auth';
|
||||
@@ -10,8 +9,6 @@ import { prompts, PromptService } from '../plugins/copilot/prompt';
|
||||
import {
|
||||
CopilotProviderFactory,
|
||||
CopilotProviderType,
|
||||
StreamObject,
|
||||
StreamObjectSchema,
|
||||
} from '../plugins/copilot/providers';
|
||||
import { TranscriptionResponseSchema } from '../plugins/copilot/transcript/types';
|
||||
import {
|
||||
@@ -186,16 +183,6 @@ const checkUrl = (url: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const checkStreamObjects = (result: string) => {
|
||||
try {
|
||||
const streamObjects = JSON.parse(result);
|
||||
z.array(StreamObjectSchema).parse(streamObjects);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const retry = async (
|
||||
action: string,
|
||||
t: ExecutionContext<Tester>,
|
||||
@@ -400,20 +387,6 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
|
||||
},
|
||||
type: 'text' as const,
|
||||
},
|
||||
{
|
||||
name: 'stream objects',
|
||||
promptName: ['Chat With AFFiNE AI'],
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: 'what is AFFiNE AI',
|
||||
},
|
||||
],
|
||||
verifier: (t: ExecutionContext<Tester>, result: string) => {
|
||||
t.truthy(checkStreamObjects(result), 'should be valid stream objects');
|
||||
},
|
||||
type: 'object' as const,
|
||||
},
|
||||
{
|
||||
name: 'Should transcribe short audio',
|
||||
promptName: ['Transcript audio'],
|
||||
@@ -707,27 +680,6 @@ for (const {
|
||||
verifier?.(t, result);
|
||||
break;
|
||||
}
|
||||
case 'object': {
|
||||
const streamObjects: StreamObject[] = [];
|
||||
for await (const chunk of provider.streamObject(
|
||||
{ modelId: prompt.model },
|
||||
[
|
||||
...prompt.finish(
|
||||
messages.reduce(
|
||||
(acc, m) => Object.assign(acc, (m as any).params || {}),
|
||||
{}
|
||||
)
|
||||
),
|
||||
...messages,
|
||||
],
|
||||
finalConfig
|
||||
)) {
|
||||
streamObjects.push(chunk);
|
||||
}
|
||||
t.truthy(streamObjects, 'should return result');
|
||||
verifier?.(t, JSON.stringify(streamObjects));
|
||||
break;
|
||||
}
|
||||
case 'image': {
|
||||
const finalMessage = [...messages];
|
||||
const params = {};
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
array2sse,
|
||||
audioTranscription,
|
||||
chatWithImages,
|
||||
chatWithStreamObject,
|
||||
chatWithText,
|
||||
chatWithTextStream,
|
||||
chatWithWorkflow,
|
||||
@@ -513,28 +512,6 @@ test('should be able to chat with api', async t => {
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
id,
|
||||
randomUUID(),
|
||||
textPromptName
|
||||
);
|
||||
const messageId = await createCopilotMessage(app, sessionId);
|
||||
|
||||
const ret4 = await chatWithStreamObject(app, sessionId, messageId);
|
||||
|
||||
const objects = Array.from('generate text to object stream').map(data =>
|
||||
JSON.stringify({ type: 'text-delta', textDelta: data })
|
||||
);
|
||||
|
||||
t.is(
|
||||
ret4,
|
||||
textToEventStream(objects, messageId),
|
||||
'should be able to chat with stream object'
|
||||
);
|
||||
}
|
||||
|
||||
Sinon.restore();
|
||||
});
|
||||
|
||||
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
# Snapshot report for `src/__tests__/e2e/doc-service/controller.spec.ts`
|
||||
|
||||
The actual snapshot is saved in `controller.spec.ts.snap`.
|
||||
|
||||
Generated by [AVA](https://avajs.dev).
|
||||
|
||||
## should get doc markdown success
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
||Title|Tag|␊
|
||||
|---|---|---|␊
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
|
||||
|For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>|␊
|
||||
|Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Trello with their Kanban|Trello with their Kanban|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
|
||||
␊
|
||||
␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
|
||||
## should get doc markdown return null when doc not exists
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
code: 'Not Found',
|
||||
message: 'Doc not found',
|
||||
name: 'NOT_FOUND',
|
||||
status: 404,
|
||||
type: 'RESOURCE_NOT_FOUND',
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -1,42 +0,0 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { CryptoHelper } from '../../../base';
|
||||
import { app, e2e, Mockers } from '../test';
|
||||
|
||||
const crypto = app.get(CryptoHelper);
|
||||
|
||||
e2e('should get doc markdown success', async t => {
|
||||
const owner = await app.signup();
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner,
|
||||
});
|
||||
|
||||
const docSnapshot = await app.create(Mockers.DocSnapshot, {
|
||||
workspaceId: workspace.id,
|
||||
user: owner,
|
||||
});
|
||||
|
||||
const res = await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docSnapshot.id}/markdown`)
|
||||
.set('x-access-token', crypto.sign(docSnapshot.id))
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
|
||||
t.snapshot(res.body);
|
||||
});
|
||||
|
||||
e2e('should get doc markdown return null when doc not exists', async t => {
|
||||
const owner = await app.signup();
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner,
|
||||
});
|
||||
|
||||
const docId = randomUUID();
|
||||
const res = await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docId}/markdown`)
|
||||
.set('x-access-token', crypto.sign(docId))
|
||||
.expect(404)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
|
||||
t.snapshot(res.body);
|
||||
});
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ModelInputType,
|
||||
ModelOutputType,
|
||||
PromptMessage,
|
||||
StreamObject,
|
||||
} from '../../plugins/copilot/providers';
|
||||
import {
|
||||
DEFAULT_DIMENSIONS,
|
||||
@@ -24,7 +23,7 @@ export class MockCopilotProvider extends OpenAIProvider {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
defaultForOutputType: true,
|
||||
},
|
||||
],
|
||||
@@ -44,7 +43,7 @@ export class MockCopilotProvider extends OpenAIProvider {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -53,7 +52,7 @@ export class MockCopilotProvider extends OpenAIProvider {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -62,7 +61,7 @@ export class MockCopilotProvider extends OpenAIProvider {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -71,7 +70,7 @@ export class MockCopilotProvider extends OpenAIProvider {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -80,11 +79,7 @@ export class MockCopilotProvider extends OpenAIProvider {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -99,15 +94,11 @@ export class MockCopilotProvider extends OpenAIProvider {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'gemini-2.5-flash',
|
||||
id: 'gemini-2.5-flash-preview-05-20',
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -204,24 +195,4 @@ export class MockCopilotProvider extends OpenAIProvider {
|
||||
await sleep(100);
|
||||
return [Array.from(randomBytes(options.dimensions)).map(v => v % 128)];
|
||||
}
|
||||
|
||||
override async *streamObject(
|
||||
cond: ModelConditions,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotChatOptions = {}
|
||||
): AsyncIterable<StreamObject> {
|
||||
const fullCond = { ...cond, outputType: ModelOutputType.Object };
|
||||
await this.checkParams({ messages, cond: fullCond, options });
|
||||
|
||||
// make some time gap for history test case
|
||||
await sleep(100);
|
||||
|
||||
const result = 'generate text to object stream';
|
||||
for (const data of result) {
|
||||
yield { type: 'text-delta', textDelta: data } as const;
|
||||
if (options.signal?.aborted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ export type MockDocSnapshotInput = {
|
||||
docId?: string;
|
||||
blob?: Uint8Array;
|
||||
updatedAt?: Date;
|
||||
snapshotFile?: string;
|
||||
};
|
||||
|
||||
export type MockedDocSnapshot = Snapshot;
|
||||
@@ -24,10 +23,7 @@ export class MockDocSnapshot extends Mocker<
|
||||
override async create(input: MockDocSnapshotInput) {
|
||||
if (!input.blob) {
|
||||
const snapshot = await readFile(
|
||||
path.join(
|
||||
import.meta.dirname,
|
||||
`../__fixtures__/${input.snapshotFile ?? 'test-doc.snapshot.bin'}`
|
||||
)
|
||||
path.join(import.meta.dirname, '../__fixtures__/test-doc.snapshot.bin')
|
||||
);
|
||||
input.blob = snapshot;
|
||||
}
|
||||
|
||||
@@ -582,14 +582,6 @@ export async function chatWithImages(
|
||||
return chatWithText(app, sessionId, messageId, '/images');
|
||||
}
|
||||
|
||||
export async function chatWithStreamObject(
|
||||
app: TestingApp,
|
||||
sessionId: string,
|
||||
messageId?: string
|
||||
) {
|
||||
return chatWithText(app, sessionId, messageId, '/stream-object');
|
||||
}
|
||||
|
||||
export async function unsplashSearch(
|
||||
app: TestingApp,
|
||||
params: Record<string, string> = {}
|
||||
|
||||
@@ -20,12 +20,6 @@ export class SocketIoAdapter extends IoAdapter {
|
||||
const server: Server = super.createIOServer(port, {
|
||||
...config,
|
||||
...options,
|
||||
// Enable CORS for Socket.IO
|
||||
cors: {
|
||||
origin: true, // Allow all origins
|
||||
credentials: true, // Allow credentials (cookies, auth headers)
|
||||
methods: ['GET', 'POST'],
|
||||
},
|
||||
});
|
||||
|
||||
if (config.canActivate) {
|
||||
|
||||
@@ -175,7 +175,6 @@ test('should get doc content in json format', async t => {
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docId}/content`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect({
|
||||
title: 'test title',
|
||||
summary: 'test summary',
|
||||
@@ -185,7 +184,6 @@ test('should get doc content in json format', async t => {
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docId}/content?full=false`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect({
|
||||
title: 'test title',
|
||||
summary: 'test summary',
|
||||
@@ -207,7 +205,6 @@ test('should get full doc content in json format', async t => {
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docId}/content?full=true`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect({
|
||||
title: 'test title',
|
||||
summary: 'test summary full',
|
||||
@@ -254,44 +251,3 @@ test('should get workspace content in json format', async t => {
|
||||
});
|
||||
t.pass();
|
||||
});
|
||||
|
||||
test('should get doc markdown in json format', async t => {
|
||||
const { app } = t.context;
|
||||
mock.method(t.context.databaseDocReader, 'getDocMarkdown', async () => {
|
||||
return {
|
||||
title: 'test title',
|
||||
markdown: 'test markdown',
|
||||
};
|
||||
});
|
||||
|
||||
const docId = randomUUID();
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docId}/markdown`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect(200)
|
||||
.expect({
|
||||
title: 'test title',
|
||||
markdown: 'test markdown',
|
||||
});
|
||||
t.pass();
|
||||
});
|
||||
|
||||
test('should 404 when doc markdown not found', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const workspaceId = '123';
|
||||
const docId = '123';
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspaceId}/docs/${docId}/markdown`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect({
|
||||
status: 404,
|
||||
code: 'Not Found',
|
||||
type: 'RESOURCE_NOT_FOUND',
|
||||
name: 'NOT_FOUND',
|
||||
message: 'Doc not found',
|
||||
})
|
||||
.expect(404);
|
||||
t.pass();
|
||||
});
|
||||
|
||||
@@ -42,20 +42,6 @@ export class DocRpcController {
|
||||
res.send(doc.bin);
|
||||
}
|
||||
|
||||
@SkipThrottle()
|
||||
@Internal()
|
||||
@Get('/workspaces/:workspaceId/docs/:docId/markdown')
|
||||
async getDocMarkdown(
|
||||
@Param('workspaceId') workspaceId: string,
|
||||
@Param('docId') docId: string
|
||||
) {
|
||||
const result = await this.docReader.getDocMarkdown(workspaceId, docId);
|
||||
if (!result) {
|
||||
throw new NotFound('Doc not found');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SkipThrottle()
|
||||
@Internal()
|
||||
@Post('/workspaces/:workspaceId/docs/:docId/diff')
|
||||
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
# Snapshot report for `src/core/doc/__tests__/reader-from-database.spec.ts`
|
||||
|
||||
The actual snapshot is saved in `reader-from-database.spec.ts.snap`.
|
||||
|
||||
Generated by [AVA](https://avajs.dev).
|
||||
|
||||
## should return doc markdown success
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
||Title|Tag|␊
|
||||
|---|---|---|␊
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
|
||||
|For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>|␊
|
||||
|Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Trello with their Kanban|Trello with their Kanban|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
|
||||
␊
|
||||
␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
BIN
Binary file not shown.
-106
@@ -1,106 +0,0 @@
|
||||
# Snapshot report for `src/core/doc/__tests__/reader-from-rpc.spec.ts`
|
||||
|
||||
The actual snapshot is saved in `reader-from-rpc.spec.ts.snap`.
|
||||
|
||||
Generated by [AVA](https://avajs.dev).
|
||||
|
||||
## should return doc markdown success
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
||Title|Tag|␊
|
||||
|---|---|---|␊
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
|
||||
|For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>|␊
|
||||
|Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Trello with their Kanban|Trello with their Kanban|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
|
||||
␊
|
||||
␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -257,28 +257,3 @@ test('should get workspace content with custom avatar', async t => {
|
||||
avatarUrl: `http://localhost:3010/api/workspaces/${workspace.id}/blobs/${avatarKey}`,
|
||||
});
|
||||
});
|
||||
|
||||
test('should return doc markdown success', async t => {
|
||||
const workspace = await module.create(Mockers.Workspace, {
|
||||
owner: user,
|
||||
name: '',
|
||||
});
|
||||
|
||||
const docSnapshot = await module.create(Mockers.DocSnapshot, {
|
||||
workspaceId: workspace.id,
|
||||
user,
|
||||
});
|
||||
|
||||
const result = await docReader.getDocMarkdown(workspace.id, docSnapshot.id);
|
||||
t.snapshot(result);
|
||||
});
|
||||
|
||||
test('should read markdown return null when doc not exists', async t => {
|
||||
const workspace = await module.create(Mockers.Workspace, {
|
||||
owner: user,
|
||||
name: '',
|
||||
});
|
||||
|
||||
const result = await docReader.getDocMarkdown(workspace.id, randomUUID());
|
||||
t.is(result, null);
|
||||
});
|
||||
|
||||
@@ -5,24 +5,13 @@ import { User, Workspace } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||
|
||||
import { createModule } from '../../../__tests__/create-module';
|
||||
import { Mockers } from '../../../__tests__/mocks';
|
||||
import { createTestingApp, type TestingApp } from '../../../__tests__/utils';
|
||||
import { UserFriendlyError } from '../../../base';
|
||||
import { ConfigFactory } from '../../../base/config';
|
||||
import { Models } from '../../../models';
|
||||
import {
|
||||
DatabaseDocReader,
|
||||
DocReader,
|
||||
DocStorageModule,
|
||||
PgWorkspaceDocStorageAdapter,
|
||||
} from '..';
|
||||
import { DatabaseDocReader, DocReader, PgWorkspaceDocStorageAdapter } from '..';
|
||||
import { RpcDocReader } from '../reader';
|
||||
|
||||
const module = await createModule({
|
||||
imports: [DocStorageModule],
|
||||
});
|
||||
|
||||
const test = ava as TestFn<{
|
||||
models: Models;
|
||||
app: TestingApp;
|
||||
@@ -79,12 +68,6 @@ test.afterEach.always(() => {
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
await t.context.docApp.close();
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('should be rpc reader', async t => {
|
||||
const { docReader } = t.context;
|
||||
t.true(docReader instanceof RpcDocReader);
|
||||
});
|
||||
|
||||
test('should return null when doc not found', async t => {
|
||||
@@ -161,6 +144,7 @@ test('should fallback to database doc reader when endpoint network error', async
|
||||
|
||||
test('should return doc when found', async t => {
|
||||
const { docReader } = t.context;
|
||||
t.true(docReader instanceof RpcDocReader);
|
||||
|
||||
const docId = randomUUID();
|
||||
const timestamp = Date.now();
|
||||
@@ -375,32 +359,3 @@ test('should return null when workspace bin meta not exists', async t => {
|
||||
const notExists = await docReader.getWorkspaceContent(randomUUID());
|
||||
t.is(notExists, null);
|
||||
});
|
||||
|
||||
test('should return doc markdown success', async t => {
|
||||
const { docReader } = t.context;
|
||||
|
||||
const workspace = await module.create(Mockers.Workspace, {
|
||||
owner: user,
|
||||
name: '',
|
||||
});
|
||||
|
||||
const docSnapshot = await module.create(Mockers.DocSnapshot, {
|
||||
workspaceId: workspace.id,
|
||||
user,
|
||||
});
|
||||
|
||||
const result = await docReader.getDocMarkdown(workspace.id, docSnapshot.id);
|
||||
t.snapshot(result);
|
||||
});
|
||||
|
||||
test('should read markdown return null when doc not exists', async t => {
|
||||
const { docReader } = t.context;
|
||||
|
||||
const workspace = await module.create(Mockers.Workspace, {
|
||||
owner: user,
|
||||
name: '',
|
||||
});
|
||||
|
||||
const result = await docReader.getDocMarkdown(workspace.id, randomUUID());
|
||||
t.is(result, null);
|
||||
});
|
||||
|
||||
@@ -18,7 +18,6 @@ import { Models } from '../../models';
|
||||
import { WorkspaceBlobStorage } from '../storage';
|
||||
import {
|
||||
type PageDocContent,
|
||||
parseDocToMarkdownFromDocSnapshot,
|
||||
parsePageDoc,
|
||||
parseWorkspaceDoc,
|
||||
} from '../utils/blocksuite';
|
||||
@@ -34,11 +33,6 @@ export interface WorkspaceDocInfo {
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export interface DocMarkdown {
|
||||
title: string;
|
||||
markdown: string;
|
||||
}
|
||||
|
||||
export abstract class DocReader {
|
||||
protected readonly logger = new Logger(DocReader.name);
|
||||
|
||||
@@ -65,11 +59,6 @@ export abstract class DocReader {
|
||||
docId: string
|
||||
): Promise<DocRecord | null>;
|
||||
|
||||
abstract getDocMarkdown(
|
||||
workspaceId: string,
|
||||
docId: string
|
||||
): Promise<DocMarkdown | null>;
|
||||
|
||||
abstract getDocDiff(
|
||||
spaceId: string,
|
||||
docId: string,
|
||||
@@ -182,17 +171,6 @@ export class DatabaseDocReader extends DocReader {
|
||||
return await this.workspace.getDoc(workspaceId, docId);
|
||||
}
|
||||
|
||||
async getDocMarkdown(
|
||||
workspaceId: string,
|
||||
docId: string
|
||||
): Promise<DocMarkdown | null> {
|
||||
const doc = await this.workspace.getDoc(workspaceId, docId);
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
return parseDocToMarkdownFromDocSnapshot(workspaceId, docId, doc.bin);
|
||||
}
|
||||
|
||||
async getDocDiff(
|
||||
spaceId: string,
|
||||
docId: string,
|
||||
@@ -326,33 +304,6 @@ export class RpcDocReader extends DatabaseDocReader {
|
||||
}
|
||||
}
|
||||
|
||||
override async getDocMarkdown(
|
||||
workspaceId: string,
|
||||
docId: string
|
||||
): Promise<DocMarkdown | null> {
|
||||
const url = `${this.config.docService.endpoint}/rpc/workspaces/${workspaceId}/docs/${docId}/markdown`;
|
||||
const accessToken = this.crypto.sign(docId);
|
||||
try {
|
||||
const res = await this.fetch(accessToken, url, 'GET');
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
return (await res.json()) as DocMarkdown;
|
||||
} catch (e) {
|
||||
if (e instanceof UserFriendlyError) {
|
||||
throw e;
|
||||
}
|
||||
const err = e as Error;
|
||||
// other error
|
||||
this.logger.error(
|
||||
`Failed to fetch doc markdown ${url}, fallback to database doc reader`,
|
||||
err
|
||||
);
|
||||
// fallback to database doc reader if the error is not user friendly, like network error
|
||||
return await super.getDocMarkdown(workspaceId, docId);
|
||||
}
|
||||
}
|
||||
|
||||
override async getDocDiff(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
|
||||
@@ -646,93 +646,6 @@ Generated by [AVA](https://avajs.dev).
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
|
||||
## can read blob filename from doc snapshot
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
blocks: [
|
||||
{
|
||||
additional: {
|
||||
displayMode: 'edgeless',
|
||||
},
|
||||
blockId: '4YHKIhPzAK',
|
||||
content: 'index file name',
|
||||
docId: 'doc-0',
|
||||
flavour: 'affine:page',
|
||||
yblock: {
|
||||
'prop:title': 'index file name',
|
||||
'sys:children': [
|
||||
'WypcCGdupE',
|
||||
'hZ1-cdLW5e',
|
||||
],
|
||||
'sys:flavour': 'affine:page',
|
||||
'sys:id': '4YHKIhPzAK',
|
||||
'sys:version': 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
additional: {
|
||||
displayMode: 'edgeless',
|
||||
},
|
||||
blockId: 'WypcCGdupE',
|
||||
content: [],
|
||||
docId: 'doc-0',
|
||||
flavour: 'affine:surface',
|
||||
parentBlockId: '4YHKIhPzAK',
|
||||
parentFlavour: 'affine:page',
|
||||
yblock: {
|
||||
'prop:elements': {
|
||||
type: '$blocksuite:internal:native$',
|
||||
value: {},
|
||||
},
|
||||
'sys:children': [],
|
||||
'sys:flavour': 'affine:surface',
|
||||
'sys:id': 'WypcCGdupE',
|
||||
'sys:version': 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
additional: {
|
||||
displayMode: 'page',
|
||||
noteBlockId: 'hZ1-cdLW5e',
|
||||
},
|
||||
blob: [
|
||||
'ldZMrM4PDlsNG4Q4YvCsz623h6TKu4qI9_FpTqIypfw=',
|
||||
],
|
||||
blockId: 'tfz1yFZdnn',
|
||||
content: 'test file name here.txt',
|
||||
docId: 'doc-0',
|
||||
flavour: 'affine:attachment',
|
||||
parentBlockId: 'hZ1-cdLW5e',
|
||||
parentFlavour: 'affine:note',
|
||||
yblock: {
|
||||
'prop:embed': false,
|
||||
'prop:footnoteIdentifier': null,
|
||||
'prop:index': 'a0',
|
||||
'prop:lockedBySelf': false,
|
||||
'prop:meta:createdAt': 1750036953927,
|
||||
'prop:meta:createdBy': '46ce597c-098a-4c61-a106-ce79827ec1de',
|
||||
'prop:meta:updatedAt': 1750036953928,
|
||||
'prop:meta:updatedBy': '46ce597c-098a-4c61-a106-ce79827ec1de',
|
||||
'prop:name': 'test file name here.txt',
|
||||
'prop:rotate': 0,
|
||||
'prop:size': 3,
|
||||
'prop:sourceId': 'ldZMrM4PDlsNG4Q4YvCsz623h6TKu4qI9_FpTqIypfw=',
|
||||
'prop:style': 'horizontalThin',
|
||||
'prop:type': 'text/plain',
|
||||
'prop:xywh': '[0,0,0,0]',
|
||||
'sys:children': [],
|
||||
'sys:flavour': 'affine:attachment',
|
||||
'sys:id': 'tfz1yFZdnn',
|
||||
'sys:version': 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
summary: '',
|
||||
title: 'index file name',
|
||||
}
|
||||
|
||||
## can read all blocks from doc snapshot without workspace snapshot
|
||||
|
||||
> Snapshot 1
|
||||
@@ -1366,104 +1279,3 @@ Generated by [AVA](https://avajs.dev).
|
||||
summary: 'AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro. You own your data, with no compromisesLocal-first & Real-time collaborativeWe love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.Blocks that assemble your next docs, tasks kanban or whiteboardThere is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further. ',
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
|
||||
## can parse doc to markdown from doc snapshot
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
||Title|Tag|␊
|
||||
|---|---|---|␊
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
|
||||
|For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>|␊
|
||||
|Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Trello with their Kanban|Trello with their Kanban|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
|
||||
␊
|
||||
␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -5,7 +5,6 @@ import { createModule } from '../../../__tests__/create-module';
|
||||
import { Mockers } from '../../../__tests__/mocks';
|
||||
import { Models } from '../../../models';
|
||||
import {
|
||||
parseDocToMarkdownFromDocSnapshot,
|
||||
readAllBlocksFromDocSnapshot,
|
||||
readAllDocIdsFromWorkspaceSnapshot,
|
||||
} from '../blocksuite';
|
||||
@@ -57,23 +56,6 @@ test('can read all blocks from doc snapshot', async t => {
|
||||
});
|
||||
});
|
||||
|
||||
test('can read blob filename from doc snapshot', async t => {
|
||||
const docSnapshot = await module.create(Mockers.DocSnapshot, {
|
||||
workspaceId: workspace.id,
|
||||
user: owner,
|
||||
snapshotFile: 'test-doc-with-blob.snapshot.bin',
|
||||
});
|
||||
|
||||
const result = await readAllBlocksFromDocSnapshot(
|
||||
workspace.id,
|
||||
'doc-0',
|
||||
docSnapshot.blob
|
||||
);
|
||||
|
||||
// NOTE: avoid snapshot result directly, because it will cause hanging
|
||||
t.snapshot(JSON.parse(JSON.stringify(result)));
|
||||
});
|
||||
|
||||
test('can read all blocks from doc snapshot without workspace snapshot', async t => {
|
||||
const doc = await models.doc.get(workspace.id, docSnapshot.id);
|
||||
t.truthy(doc);
|
||||
@@ -89,13 +71,3 @@ test('can read all blocks from doc snapshot without workspace snapshot', async t
|
||||
blocks: result!.blocks.map(block => omit(block, ['yblock'])),
|
||||
});
|
||||
});
|
||||
|
||||
test('can parse doc to markdown from doc snapshot', async t => {
|
||||
const result = parseDocToMarkdownFromDocSnapshot(
|
||||
workspace.id,
|
||||
docSnapshot.id,
|
||||
docSnapshot.blob
|
||||
);
|
||||
|
||||
t.snapshot(result);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- import from bundle
|
||||
import {
|
||||
parsePageDoc as parseDocToMarkdown,
|
||||
readAllBlocksFromDoc,
|
||||
readAllDocIdsFromRootDoc,
|
||||
} from '@affine/reader/dist';
|
||||
@@ -197,30 +196,3 @@ export async function readAllBlocksFromDocSnapshot(
|
||||
maxSummaryLength,
|
||||
});
|
||||
}
|
||||
|
||||
export function parseDocToMarkdownFromDocSnapshot(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
docSnapshot: Uint8Array
|
||||
) {
|
||||
const ydoc = new YDoc({
|
||||
guid: docId,
|
||||
});
|
||||
applyUpdate(ydoc, docSnapshot);
|
||||
|
||||
const parsed = parseDocToMarkdown({
|
||||
workspaceId,
|
||||
doc: ydoc,
|
||||
buildBlobUrl: (blobId: string) => {
|
||||
return `/${workspaceId}/blobs/${blobId}`;
|
||||
},
|
||||
buildDocUrl: (docId: string) => {
|
||||
return `/workspace/${workspaceId}/${docId}`;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
title: parsed.title,
|
||||
markdown: parsed.md,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
|
||||
import {
|
||||
Cache,
|
||||
@@ -16,6 +15,8 @@ import {
|
||||
Models,
|
||||
} from '../../../models';
|
||||
import { type EmbeddingClient, getEmbeddingClient } from '../embedding';
|
||||
import { PromptService } from '../prompt';
|
||||
import { CopilotProviderFactory } from '../providers';
|
||||
import { ContextSession } from './session';
|
||||
|
||||
const CONTEXT_SESSION_KEY = 'context-session';
|
||||
@@ -26,9 +27,10 @@ export class CopilotContextService implements OnApplicationBootstrap {
|
||||
private client: EmbeddingClient | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly moduleRef: ModuleRef,
|
||||
private readonly cache: Cache,
|
||||
private readonly models: Models
|
||||
private readonly models: Models,
|
||||
private readonly providerFactory: CopilotProviderFactory,
|
||||
private readonly prompt: PromptService
|
||||
) {}
|
||||
|
||||
@OnEvent('config.init')
|
||||
@@ -42,7 +44,7 @@ export class CopilotContextService implements OnApplicationBootstrap {
|
||||
}
|
||||
|
||||
private async setup() {
|
||||
this.client = await getEmbeddingClient(this.moduleRef);
|
||||
this.client = await getEmbeddingClient(this.providerFactory, this.prompt);
|
||||
}
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
@@ -163,7 +165,7 @@ export class CopilotContextService implements OnApplicationBootstrap {
|
||||
);
|
||||
if (!fileChunks.length) return [];
|
||||
|
||||
return await this.embeddingClient.reRank(content, fileChunks, topK, signal);
|
||||
return this.embeddingClient.reRank(content, fileChunks, topK, signal);
|
||||
}
|
||||
|
||||
async matchWorkspaceDocs(
|
||||
@@ -186,48 +188,7 @@ export class CopilotContextService implements OnApplicationBootstrap {
|
||||
);
|
||||
if (!workspaceChunks.length) return [];
|
||||
|
||||
return await this.embeddingClient.reRank(
|
||||
content,
|
||||
workspaceChunks,
|
||||
topK,
|
||||
signal
|
||||
);
|
||||
}
|
||||
|
||||
async matchWorkspaceAll(
|
||||
workspaceId: string,
|
||||
content: string,
|
||||
topK: number = 5,
|
||||
signal?: AbortSignal,
|
||||
threshold: number = 0.5
|
||||
) {
|
||||
if (!this.embeddingClient) return [];
|
||||
const embedding = await this.embeddingClient.getEmbedding(content, signal);
|
||||
if (!embedding) return [];
|
||||
|
||||
const [fileChunks, workspaceChunks] = await Promise.all([
|
||||
this.models.copilotWorkspace.matchFileEmbedding(
|
||||
workspaceId,
|
||||
embedding,
|
||||
topK * 2,
|
||||
threshold
|
||||
),
|
||||
this.models.copilotContext.matchWorkspaceEmbedding(
|
||||
embedding,
|
||||
workspaceId,
|
||||
topK * 2,
|
||||
threshold
|
||||
),
|
||||
]);
|
||||
|
||||
if (!fileChunks.length && !workspaceChunks.length) return [];
|
||||
|
||||
return await this.embeddingClient.reRank(
|
||||
content,
|
||||
[...fileChunks, ...workspaceChunks],
|
||||
topK,
|
||||
signal
|
||||
);
|
||||
return this.embeddingClient.reRank(content, workspaceChunks, topK, signal);
|
||||
}
|
||||
|
||||
@OnEvent('workspace.doc.embed.failed')
|
||||
|
||||
@@ -51,7 +51,6 @@ import {
|
||||
ModelInputType,
|
||||
ModelOutputType,
|
||||
} from './providers';
|
||||
import { StreamObjectParser } from './providers/utils';
|
||||
import { ChatSession, ChatSessionService } from './session';
|
||||
import { CopilotStorage } from './storage';
|
||||
import { ChatMessage, ChatQuerySchema } from './types';
|
||||
@@ -190,45 +189,6 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
return merge(source$.pipe(finalize(() => subject$.next(null))), ping$);
|
||||
}
|
||||
|
||||
private async prepareChatSession(
|
||||
user: CurrentUser,
|
||||
sessionId: string,
|
||||
query: Record<string, string | string[]>,
|
||||
outputType: ModelOutputType
|
||||
) {
|
||||
let { messageId, retry, modelId, params } = ChatQuerySchema.parse(query);
|
||||
|
||||
const { provider, model } = await this.chooseProvider(
|
||||
outputType,
|
||||
user.id,
|
||||
sessionId,
|
||||
messageId,
|
||||
modelId
|
||||
);
|
||||
|
||||
const [latestMessage, session] = await this.appendSessionMessage(
|
||||
sessionId,
|
||||
messageId,
|
||||
retry
|
||||
);
|
||||
|
||||
if (latestMessage) {
|
||||
params = Object.assign({}, params, latestMessage.params, {
|
||||
content: latestMessage.content,
|
||||
attachments: latestMessage.attachments,
|
||||
});
|
||||
}
|
||||
|
||||
const finalMessage = session.finish(params);
|
||||
|
||||
return {
|
||||
provider,
|
||||
model,
|
||||
session,
|
||||
finalMessage,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/chat/:sessionId')
|
||||
@CallMetric('ai', 'chat', { timer: true })
|
||||
async chat(
|
||||
@@ -240,24 +200,40 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
const info: any = { sessionId, params: query };
|
||||
|
||||
try {
|
||||
const { provider, model, session, finalMessage } =
|
||||
await this.prepareChatSession(
|
||||
user,
|
||||
sessionId,
|
||||
query,
|
||||
ModelOutputType.Text
|
||||
);
|
||||
let { messageId, retry, reasoning, webSearch, modelId, params } =
|
||||
ChatQuerySchema.parse(query);
|
||||
|
||||
const { provider, model } = await this.chooseProvider(
|
||||
ModelOutputType.Text,
|
||||
user.id,
|
||||
sessionId,
|
||||
messageId,
|
||||
modelId
|
||||
);
|
||||
|
||||
const [latestMessage, session] = await this.appendSessionMessage(
|
||||
sessionId,
|
||||
messageId,
|
||||
retry
|
||||
);
|
||||
|
||||
info.model = model;
|
||||
info.finalMessage = finalMessage.filter(m => m.role !== 'system');
|
||||
metrics.ai.counter('chat_calls').add(1, { model });
|
||||
|
||||
const { reasoning, webSearch } = ChatQuerySchema.parse(query);
|
||||
if (latestMessage) {
|
||||
params = Object.assign({}, params, latestMessage.params, {
|
||||
content: latestMessage.content,
|
||||
attachments: latestMessage.attachments,
|
||||
});
|
||||
}
|
||||
|
||||
const finalMessage = session.finish(params);
|
||||
info.finalMessage = finalMessage.filter(m => m.role !== 'system');
|
||||
|
||||
const content = await provider.text({ modelId: model }, finalMessage, {
|
||||
...session.config.promptConfig,
|
||||
signal: this.getSignal(req),
|
||||
user: user.id,
|
||||
workspace: session.config.workspaceId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
});
|
||||
@@ -292,26 +268,42 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
const info: any = { sessionId, params: query, throwInStream: false };
|
||||
|
||||
try {
|
||||
const { provider, model, session, finalMessage } =
|
||||
await this.prepareChatSession(
|
||||
user,
|
||||
sessionId,
|
||||
query,
|
||||
ModelOutputType.Text
|
||||
);
|
||||
let { messageId, retry, reasoning, webSearch, modelId, params } =
|
||||
ChatQuerySchema.parse(query);
|
||||
|
||||
const { provider, model } = await this.chooseProvider(
|
||||
ModelOutputType.Text,
|
||||
user.id,
|
||||
sessionId,
|
||||
messageId,
|
||||
modelId
|
||||
);
|
||||
|
||||
const [latestMessage, session] = await this.appendSessionMessage(
|
||||
sessionId,
|
||||
messageId,
|
||||
retry
|
||||
);
|
||||
|
||||
info.model = model;
|
||||
info.finalMessage = finalMessage.filter(m => m.role !== 'system');
|
||||
metrics.ai.counter('chat_stream_calls').add(1, { model });
|
||||
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
|
||||
|
||||
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
|
||||
if (latestMessage) {
|
||||
params = Object.assign({}, params, latestMessage.params, {
|
||||
content: latestMessage.content,
|
||||
attachments: latestMessage.attachments,
|
||||
});
|
||||
}
|
||||
|
||||
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
|
||||
const finalMessage = session.finish(params);
|
||||
info.finalMessage = finalMessage.filter(m => m.role !== 'system');
|
||||
|
||||
const source$ = from(
|
||||
provider.streamText({ modelId: model }, finalMessage, {
|
||||
...session.config.promptConfig,
|
||||
signal: this.getSignal(req),
|
||||
user: user.id,
|
||||
workspace: session.config.workspaceId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
})
|
||||
@@ -354,83 +346,6 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
@Sse('/chat/:sessionId/stream-object')
|
||||
@CallMetric('ai', 'chat_object_stream', { timer: true })
|
||||
async chatStreamObject(
|
||||
@CurrentUser() user: CurrentUser,
|
||||
@Req() req: Request,
|
||||
@Param('sessionId') sessionId: string,
|
||||
@Query() query: Record<string, string>
|
||||
): Promise<Observable<ChatEvent>> {
|
||||
const info: any = { sessionId, params: query, throwInStream: false };
|
||||
|
||||
try {
|
||||
const { provider, model, session, finalMessage } =
|
||||
await this.prepareChatSession(
|
||||
user,
|
||||
sessionId,
|
||||
query,
|
||||
ModelOutputType.Object
|
||||
);
|
||||
|
||||
info.model = model;
|
||||
info.finalMessage = finalMessage.filter(m => m.role !== 'system');
|
||||
metrics.ai.counter('chat_object_stream_calls').add(1, { model });
|
||||
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
|
||||
|
||||
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
|
||||
const source$ = from(
|
||||
provider.streamObject({ modelId: model }, finalMessage, {
|
||||
...session.config.promptConfig,
|
||||
signal: this.getSignal(req),
|
||||
user: user.id,
|
||||
workspace: session.config.workspaceId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
})
|
||||
).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 => {
|
||||
const parser = new StreamObjectParser();
|
||||
const streamObjects = parser.mergeTextDelta(values);
|
||||
const content = parser.mergeContent(streamObjects);
|
||||
session.push({
|
||||
role: 'assistant',
|
||||
content,
|
||||
streamObjects,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
return from(session.save());
|
||||
}),
|
||||
mergeMap(() => EMPTY)
|
||||
)
|
||||
)
|
||||
),
|
||||
catchError(e => {
|
||||
metrics.ai.counter('chat_object_stream_errors').add(1);
|
||||
info.throwInStream = true;
|
||||
return mapSseError(e, info);
|
||||
}),
|
||||
finalize(() => {
|
||||
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value - 1);
|
||||
})
|
||||
);
|
||||
|
||||
return this.mergePingStream(messageId || '', source$);
|
||||
} catch (err) {
|
||||
metrics.ai.counter('chat_object_stream_errors').add(1, info);
|
||||
return mapSseError(err, info);
|
||||
}
|
||||
}
|
||||
|
||||
@Sse('/chat/:sessionId/workflow')
|
||||
@CallMetric('ai', 'chat_workflow', { timer: true })
|
||||
async chatWorkflow(
|
||||
@@ -463,7 +378,6 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
...session.config.promptConfig,
|
||||
signal: this.getSignal(req),
|
||||
user: user.id,
|
||||
workspace: session.config.workspaceId,
|
||||
})
|
||||
).pipe(
|
||||
connect(shared$ =>
|
||||
@@ -586,7 +500,6 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
seed: this.parseNumber(params.seed),
|
||||
signal: this.getSignal(req),
|
||||
user: user.id,
|
||||
workspace: session.config.workspaceId,
|
||||
}
|
||||
)
|
||||
).pipe(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import type { ModuleRef } from '@nestjs/core';
|
||||
|
||||
import {
|
||||
CopilotPromptNotFound,
|
||||
@@ -194,16 +193,12 @@ class ProductionEmbeddingClient extends EmbeddingClient {
|
||||
|
||||
let EMBEDDING_CLIENT: EmbeddingClient | undefined;
|
||||
export async function getEmbeddingClient(
|
||||
moduleRef: ModuleRef
|
||||
providerFactory: CopilotProviderFactory,
|
||||
prompt: PromptService
|
||||
): Promise<EmbeddingClient | undefined> {
|
||||
if (EMBEDDING_CLIENT) {
|
||||
return EMBEDDING_CLIENT;
|
||||
}
|
||||
const providerFactory = moduleRef.get(CopilotProviderFactory, {
|
||||
strict: false,
|
||||
});
|
||||
const prompt = moduleRef.get(PromptService, { strict: false });
|
||||
|
||||
const client = new ProductionEmbeddingClient(providerFactory, prompt);
|
||||
if (await client.configured()) {
|
||||
EMBEDDING_CLIENT = client;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
|
||||
import {
|
||||
AFFiNELogger,
|
||||
@@ -15,6 +14,8 @@ import {
|
||||
} from '../../../base';
|
||||
import { DocReader } from '../../../core/doc';
|
||||
import { Models } from '../../../models';
|
||||
import { PromptService } from '../prompt';
|
||||
import { CopilotProviderFactory } from '../providers';
|
||||
import { CopilotStorage } from '../storage';
|
||||
import { readStream } from '../utils';
|
||||
import { getEmbeddingClient } from './client';
|
||||
@@ -30,11 +31,12 @@ export class CopilotEmbeddingJob {
|
||||
private client: EmbeddingClient | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly moduleRef: ModuleRef,
|
||||
private readonly doc: DocReader,
|
||||
private readonly event: EventBus,
|
||||
private readonly logger: AFFiNELogger,
|
||||
private readonly models: Models,
|
||||
private readonly providerFactory: CopilotProviderFactory,
|
||||
private readonly prompt: PromptService,
|
||||
private readonly queue: JobQueue,
|
||||
private readonly storage: CopilotStorage
|
||||
) {
|
||||
@@ -55,7 +57,7 @@ export class CopilotEmbeddingJob {
|
||||
this.supportEmbedding =
|
||||
await this.models.copilotContext.checkEmbeddingAvailable();
|
||||
if (this.supportEmbedding) {
|
||||
this.client = await getEmbeddingClient(this.moduleRef);
|
||||
this.client = await getEmbeddingClient(this.providerFactory, this.prompt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -303,7 +303,7 @@ const textActions: Prompt[] = [
|
||||
{
|
||||
name: 'Transcript audio',
|
||||
action: 'Transcript audio',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -539,7 +539,7 @@ A concise paragraph that captures the article's main argument and key conclusion
|
||||
{
|
||||
name: 'Explain this code',
|
||||
action: 'Explain this code',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -590,7 +590,7 @@ A concise paragraph that captures the article's main argument and key conclusion
|
||||
{
|
||||
name: 'Translate to',
|
||||
action: 'Translate',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -714,7 +714,7 @@ You are an assistant helping find actions of meeting summary. Use this format, r
|
||||
{
|
||||
name: 'Write an article about this',
|
||||
action: 'Write an article about this',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -806,7 +806,7 @@ You are an assistant helping find actions of meeting summary. Use this format, r
|
||||
{
|
||||
name: 'Write a poem about this',
|
||||
action: 'Write a poem about this',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -855,7 +855,7 @@ You are an assistant helping find actions of meeting summary. Use this format, r
|
||||
{
|
||||
name: 'Write a blog post about this',
|
||||
action: 'Write a blog post about this',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -906,7 +906,7 @@ You are an assistant helping find actions of meeting summary. Use this format, r
|
||||
{
|
||||
name: 'Write outline',
|
||||
action: 'Write outline',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -980,7 +980,7 @@ You are an assistant helping find actions of meeting summary. Use this format, r
|
||||
{
|
||||
name: 'Brainstorm ideas about this',
|
||||
action: 'Brainstorm ideas about this',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -1074,7 +1074,7 @@ You are an assistant helping find actions of meeting summary. Use this format, r
|
||||
{
|
||||
name: 'Improve writing for it',
|
||||
action: 'Improve writing for it',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -1146,7 +1146,7 @@ The output must be perfect. Adherence to every detail of these instructions is n
|
||||
{
|
||||
name: 'Fix spelling for it',
|
||||
action: 'Fix spelling for it',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -1300,7 +1300,7 @@ If there are items in the content that can be used as to-do tasks, please refer
|
||||
{
|
||||
name: 'Create headings',
|
||||
action: 'Create headings',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -1408,7 +1408,7 @@ When sent new notes, respond ONLY with the contents of the html file.`,
|
||||
{
|
||||
name: 'Make it longer',
|
||||
action: 'Make it longer',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -1433,7 +1433,7 @@ When sent new notes, respond ONLY with the contents of the html file.`,
|
||||
{
|
||||
name: 'Make it shorter',
|
||||
action: 'Make it shorter',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -1458,7 +1458,7 @@ When sent new notes, respond ONLY with the contents of the html file.`,
|
||||
{
|
||||
name: 'Continue writing',
|
||||
action: 'Continue writing',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-flash-preview-05-20',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -1654,8 +1654,8 @@ const CHAT_PROMPT: Omit<Prompt, 'name'> = {
|
||||
'claude-sonnet-4-20250514',
|
||||
'claude-3-7-sonnet-20250219',
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'gemini-2.5-flash',
|
||||
'gemini-2.5-pro',
|
||||
'gemini-2.5-flash-preview-05-20',
|
||||
'gemini-2.5-pro-preview-06-05',
|
||||
'claude-opus-4@20250514',
|
||||
'claude-sonnet-4@20250514',
|
||||
'claude-3-7-sonnet@20250219',
|
||||
@@ -1791,13 +1791,7 @@ Below is the user's query. Please respond in the user's preferred language witho
|
||||
},
|
||||
],
|
||||
config: {
|
||||
tools: [
|
||||
'docRead',
|
||||
'docEdit',
|
||||
'docKeywordSearch',
|
||||
'docSemanticSearch',
|
||||
'webSearch',
|
||||
],
|
||||
tools: ['webSearch'],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -10,20 +10,15 @@ import {
|
||||
metrics,
|
||||
UserFriendlyError,
|
||||
} from '../../../../base';
|
||||
import { createExaCrawlTool, createExaSearchTool } from '../../tools';
|
||||
import { CopilotProvider } from '../provider';
|
||||
import type {
|
||||
CopilotChatOptions,
|
||||
CopilotProviderModel,
|
||||
ModelConditions,
|
||||
PromptMessage,
|
||||
StreamObject,
|
||||
} from '../types';
|
||||
import { ModelOutputType } from '../types';
|
||||
import {
|
||||
chatToGPTMessage,
|
||||
StreamObjectParser,
|
||||
TextStreamParser,
|
||||
} from '../utils';
|
||||
import { chatToGPTMessage, TextStreamParser } from '../utils';
|
||||
|
||||
export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
|
||||
private readonly MAX_STEPS = 20;
|
||||
@@ -73,7 +68,7 @@ export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
|
||||
providerOptions: {
|
||||
anthropic: this.getAnthropicOptions(options, model.id),
|
||||
},
|
||||
tools: await this.getTools(options, model.id),
|
||||
tools: this.getTools(),
|
||||
maxSteps: this.MAX_STEPS,
|
||||
experimental_continueSteps: true,
|
||||
});
|
||||
@@ -98,7 +93,21 @@ export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
|
||||
|
||||
try {
|
||||
metrics.ai.counter('chat_text_stream_calls').add(1, { model: model.id });
|
||||
const fullStream = await this.getFullStream(model, messages, options);
|
||||
const [system, msgs] = await chatToGPTMessage(messages, true, true);
|
||||
|
||||
const { fullStream } = streamText({
|
||||
model: this.instance(model.id),
|
||||
system,
|
||||
messages: msgs,
|
||||
abortSignal: options.signal,
|
||||
providerOptions: {
|
||||
anthropic: this.getAnthropicOptions(options, model.id),
|
||||
},
|
||||
tools: this.getTools(),
|
||||
maxSteps: this.MAX_STEPS,
|
||||
experimental_continueSteps: true,
|
||||
});
|
||||
|
||||
const parser = new TextStreamParser();
|
||||
for await (const chunk of fullStream) {
|
||||
const result = parser.parse(chunk);
|
||||
@@ -114,58 +123,11 @@ export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
|
||||
}
|
||||
}
|
||||
|
||||
override async *streamObject(
|
||||
cond: ModelConditions,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotChatOptions = {}
|
||||
): AsyncIterable<StreamObject> {
|
||||
const fullCond = { ...cond, outputType: ModelOutputType.Object };
|
||||
await this.checkParams({ cond: fullCond, messages, options });
|
||||
const model = this.selectModel(fullCond);
|
||||
|
||||
try {
|
||||
metrics.ai
|
||||
.counter('chat_object_stream_calls')
|
||||
.add(1, { model: model.id });
|
||||
const fullStream = await this.getFullStream(model, messages, options);
|
||||
const parser = new StreamObjectParser();
|
||||
for await (const chunk of fullStream) {
|
||||
const result = parser.parse(chunk);
|
||||
if (result) {
|
||||
yield result;
|
||||
}
|
||||
if (options.signal?.aborted) {
|
||||
await fullStream.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
metrics.ai
|
||||
.counter('chat_object_stream_errors')
|
||||
.add(1, { model: model.id });
|
||||
throw this.handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getFullStream(
|
||||
model: CopilotProviderModel,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotChatOptions = {}
|
||||
) {
|
||||
const [system, msgs] = await chatToGPTMessage(messages, true, true);
|
||||
const { fullStream } = streamText({
|
||||
model: this.instance(model.id),
|
||||
system,
|
||||
messages: msgs,
|
||||
abortSignal: options.signal,
|
||||
providerOptions: {
|
||||
anthropic: this.getAnthropicOptions(options, model.id),
|
||||
},
|
||||
tools: await this.getTools(options, model.id),
|
||||
maxSteps: this.MAX_STEPS,
|
||||
experimental_continueSteps: true,
|
||||
});
|
||||
return fullStream;
|
||||
private getTools() {
|
||||
return {
|
||||
web_search_exa: createExaSearchTool(this.AFFiNEConfig),
|
||||
web_crawl_exa: createExaCrawlTool(this.AFFiNEConfig),
|
||||
};
|
||||
}
|
||||
|
||||
private getAnthropicOptions(options: CopilotChatOptions, model: string) {
|
||||
|
||||
@@ -20,7 +20,7 @@ export class AnthropicOfficialProvider extends AnthropicProvider<AnthropicOffici
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -29,7 +29,7 @@ export class AnthropicOfficialProvider extends AnthropicProvider<AnthropicOffici
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -38,7 +38,7 @@ export class AnthropicOfficialProvider extends AnthropicProvider<AnthropicOffici
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -47,7 +47,7 @@ export class AnthropicOfficialProvider extends AnthropicProvider<AnthropicOffici
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
defaultForOutputType: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -18,7 +18,7 @@ export class AnthropicVertexProvider extends AnthropicProvider<AnthropicVertexCo
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -27,7 +27,7 @@ export class AnthropicVertexProvider extends AnthropicProvider<AnthropicVertexCo
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -36,7 +36,7 @@ export class AnthropicVertexProvider extends AnthropicProvider<AnthropicVertexCo
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -45,7 +45,7 @@ export class AnthropicVertexProvider extends AnthropicProvider<AnthropicVertexCo
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
defaultForOutputType: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -180,7 +180,7 @@ export class FalProvider extends CopilotProvider<FalConfig> {
|
||||
? v.attachment
|
||||
: undefined
|
||||
)
|
||||
.find(v => !!v),
|
||||
.filter(v => !!v)[0],
|
||||
prompt: content.trim(),
|
||||
loras: lora.length ? lora : undefined,
|
||||
controlnets: controlnets.length ? controlnets : undefined,
|
||||
|
||||
@@ -21,17 +21,11 @@ import { CopilotProvider } from '../provider';
|
||||
import type {
|
||||
CopilotChatOptions,
|
||||
CopilotImageOptions,
|
||||
CopilotProviderModel,
|
||||
ModelConditions,
|
||||
PromptMessage,
|
||||
StreamObject,
|
||||
} from '../types';
|
||||
import { ModelOutputType } from '../types';
|
||||
import {
|
||||
chatToGPTMessage,
|
||||
StreamObjectParser,
|
||||
TextStreamParser,
|
||||
} from '../utils';
|
||||
import { chatToGPTMessage, TextStreamParser } from '../utils';
|
||||
|
||||
export const DEFAULT_DIMENSIONS = 256;
|
||||
|
||||
@@ -156,7 +150,21 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
|
||||
|
||||
try {
|
||||
metrics.ai.counter('chat_text_stream_calls').add(1, { model: model.id });
|
||||
const fullStream = await this.getFullStream(model, messages, options);
|
||||
const [system, msgs] = await chatToGPTMessage(messages);
|
||||
|
||||
const { fullStream } = streamText({
|
||||
model: this.instance(model.id, {
|
||||
useSearchGrounding: this.useSearchGrounding(options),
|
||||
}),
|
||||
system,
|
||||
messages: msgs,
|
||||
abortSignal: options.signal,
|
||||
maxSteps: this.MAX_STEPS,
|
||||
providerOptions: {
|
||||
google: this.getGeminiOptions(options, model.id),
|
||||
},
|
||||
});
|
||||
|
||||
const parser = new TextStreamParser();
|
||||
for await (const chunk of fullStream) {
|
||||
const result = parser.parse(chunk);
|
||||
@@ -172,60 +180,6 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
|
||||
}
|
||||
}
|
||||
|
||||
override async *streamObject(
|
||||
cond: ModelConditions,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotChatOptions = {}
|
||||
): AsyncIterable<StreamObject> {
|
||||
const fullCond = { ...cond, outputType: ModelOutputType.Object };
|
||||
await this.checkParams({ cond: fullCond, messages, options });
|
||||
const model = this.selectModel(fullCond);
|
||||
|
||||
try {
|
||||
metrics.ai
|
||||
.counter('chat_object_stream_calls')
|
||||
.add(1, { model: model.id });
|
||||
const fullStream = await this.getFullStream(model, messages, options);
|
||||
const parser = new StreamObjectParser();
|
||||
for await (const chunk of fullStream) {
|
||||
const result = parser.parse(chunk);
|
||||
if (result) {
|
||||
yield result;
|
||||
}
|
||||
if (options.signal?.aborted) {
|
||||
await fullStream.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
metrics.ai
|
||||
.counter('chat_object_stream_errors')
|
||||
.add(1, { model: model.id });
|
||||
throw this.handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getFullStream(
|
||||
model: CopilotProviderModel,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotChatOptions = {}
|
||||
) {
|
||||
const [system, msgs] = await chatToGPTMessage(messages);
|
||||
const { fullStream } = streamText({
|
||||
model: this.instance(model.id, {
|
||||
useSearchGrounding: this.useSearchGrounding(options),
|
||||
}),
|
||||
system,
|
||||
messages: msgs,
|
||||
abortSignal: options.signal,
|
||||
maxSteps: this.MAX_STEPS,
|
||||
providerOptions: {
|
||||
google: this.getGeminiOptions(options, model.id),
|
||||
},
|
||||
});
|
||||
return fullStream;
|
||||
}
|
||||
|
||||
private getGeminiOptions(options: CopilotChatOptions, model: string) {
|
||||
const result: GoogleGenerativeAIProviderOptions = {};
|
||||
if (options?.reasoning && this.isReasoningModel(model)) {
|
||||
|
||||
@@ -25,18 +25,14 @@ export class GeminiGenerativeProvider extends GeminiProvider<GeminiGenerativeCon
|
||||
ModelInputType.Image,
|
||||
ModelInputType.Audio,
|
||||
],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
defaultForOutputType: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Gemini 2.5 Flash',
|
||||
id: 'gemini-2.5-flash',
|
||||
id: 'gemini-2.5-flash-preview-05-20',
|
||||
capabilities: [
|
||||
{
|
||||
input: [
|
||||
@@ -44,17 +40,13 @@ export class GeminiGenerativeProvider extends GeminiProvider<GeminiGenerativeCon
|
||||
ModelInputType.Image,
|
||||
ModelInputType.Audio,
|
||||
],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Gemini 2.5 Pro',
|
||||
id: 'gemini-2.5-pro',
|
||||
id: 'gemini-2.5-pro-preview-06-05',
|
||||
capabilities: [
|
||||
{
|
||||
input: [
|
||||
@@ -62,11 +54,7 @@ export class GeminiGenerativeProvider extends GeminiProvider<GeminiGenerativeCon
|
||||
ModelInputType.Image,
|
||||
ModelInputType.Audio,
|
||||
],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ export class GeminiVertexProvider extends GeminiProvider<GeminiVertexConfig> {
|
||||
readonly models = [
|
||||
{
|
||||
name: 'Gemini 2.5 Flash',
|
||||
id: 'gemini-2.5-flash',
|
||||
id: 'gemini-2.5-flash-preview-05-20',
|
||||
capabilities: [
|
||||
{
|
||||
input: [
|
||||
@@ -23,17 +23,13 @@ export class GeminiVertexProvider extends GeminiProvider<GeminiVertexConfig> {
|
||||
ModelInputType.Image,
|
||||
ModelInputType.Audio,
|
||||
],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Gemini 2.5 Pro',
|
||||
id: 'gemini-2.5-pro',
|
||||
id: 'gemini-2.5-pro-preview-06-05',
|
||||
capabilities: [
|
||||
{
|
||||
input: [
|
||||
@@ -41,11 +37,7 @@ export class GeminiVertexProvider extends GeminiProvider<GeminiVertexConfig> {
|
||||
ModelInputType.Image,
|
||||
ModelInputType.Audio,
|
||||
],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
generateObject,
|
||||
generateText,
|
||||
streamText,
|
||||
Tool,
|
||||
ToolSet,
|
||||
} from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -21,25 +21,18 @@ import {
|
||||
metrics,
|
||||
UserFriendlyError,
|
||||
} from '../../../base';
|
||||
import { createExaCrawlTool, createExaSearchTool } from '../tools';
|
||||
import { CopilotProvider } from './provider';
|
||||
import type {
|
||||
CopilotChatOptions,
|
||||
CopilotChatTools,
|
||||
CopilotEmbeddingOptions,
|
||||
CopilotImageOptions,
|
||||
CopilotProviderModel,
|
||||
CopilotStructuredOptions,
|
||||
ModelConditions,
|
||||
PromptMessage,
|
||||
StreamObject,
|
||||
} from './types';
|
||||
import { CopilotProviderType, ModelInputType, ModelOutputType } from './types';
|
||||
import {
|
||||
chatToGPTMessage,
|
||||
CitationParser,
|
||||
StreamObjectParser,
|
||||
TextStreamParser,
|
||||
} from './utils';
|
||||
import { chatToGPTMessage, CitationParser, TextStreamParser } from './utils';
|
||||
|
||||
export const DEFAULT_DIMENSIONS = 256;
|
||||
|
||||
@@ -72,7 +65,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -82,7 +75,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -91,7 +84,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -101,7 +94,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -110,11 +103,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
defaultForOutputType: true,
|
||||
},
|
||||
],
|
||||
@@ -124,11 +113,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -137,11 +122,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -150,11 +131,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -163,7 +140,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -172,7 +149,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -181,7 +158,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Object],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -271,14 +248,25 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
}
|
||||
}
|
||||
|
||||
override getProviderSpecificTools(
|
||||
toolName: CopilotChatTools,
|
||||
model: string
|
||||
): [string, Tool] | undefined {
|
||||
if (toolName === 'webSearch' && !this.isReasoningModel(model)) {
|
||||
return ['web_search_preview', openai.tools.webSearchPreview()];
|
||||
private getTools(options: CopilotChatOptions, model: string): ToolSet {
|
||||
const tools: ToolSet = {};
|
||||
if (options?.tools?.length) {
|
||||
for (const tool of options.tools) {
|
||||
switch (tool) {
|
||||
case 'webSearch': {
|
||||
if (this.isReasoningModel(model)) {
|
||||
tools.web_search_exa = createExaSearchTool(this.AFFiNEConfig);
|
||||
tools.web_crawl_exa = createExaCrawlTool(this.AFFiNEConfig);
|
||||
} else {
|
||||
tools.web_search_preview = openai.tools.webSearchPreview();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
return;
|
||||
return tools;
|
||||
}
|
||||
|
||||
async text(
|
||||
@@ -309,7 +297,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
providerOptions: {
|
||||
openai: this.getOpenAIOptions(options, model.id),
|
||||
},
|
||||
tools: await this.getTools(options, model.id),
|
||||
tools: this.getTools(options, model.id),
|
||||
maxSteps: this.MAX_STEPS,
|
||||
abortSignal: options.signal,
|
||||
});
|
||||
@@ -335,7 +323,26 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
|
||||
try {
|
||||
metrics.ai.counter('chat_text_stream_calls').add(1, { model: model.id });
|
||||
const fullStream = await this.getFullStream(model, messages, options);
|
||||
const [system, msgs] = await chatToGPTMessage(messages);
|
||||
|
||||
const modelInstance = this.#instance.responses(model.id);
|
||||
|
||||
const { fullStream } = streamText({
|
||||
model: modelInstance,
|
||||
system,
|
||||
messages: msgs,
|
||||
frequencyPenalty: options.frequencyPenalty ?? 0,
|
||||
presencePenalty: options.presencePenalty ?? 0,
|
||||
temperature: options.temperature ?? 0,
|
||||
maxTokens: options.maxTokens ?? 4096,
|
||||
providerOptions: {
|
||||
openai: this.getOpenAIOptions(options, model.id),
|
||||
},
|
||||
tools: this.getTools(options, model.id),
|
||||
maxSteps: this.MAX_STEPS,
|
||||
abortSignal: options.signal,
|
||||
});
|
||||
|
||||
const citationParser = new CitationParser();
|
||||
const textParser = new TextStreamParser();
|
||||
for await (const chunk of fullStream) {
|
||||
@@ -367,39 +374,6 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
}
|
||||
}
|
||||
|
||||
override async *streamObject(
|
||||
cond: ModelConditions,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotChatOptions = {}
|
||||
): AsyncIterable<StreamObject> {
|
||||
const fullCond = { ...cond, outputType: ModelOutputType.Object };
|
||||
await this.checkParams({ cond: fullCond, messages, options });
|
||||
const model = this.selectModel(fullCond);
|
||||
|
||||
try {
|
||||
metrics.ai
|
||||
.counter('chat_object_stream_calls')
|
||||
.add(1, { model: model.id });
|
||||
const fullStream = await this.getFullStream(model, messages, options);
|
||||
const parser = new StreamObjectParser();
|
||||
for await (const chunk of fullStream) {
|
||||
const result = parser.parse(chunk);
|
||||
if (result) {
|
||||
yield result;
|
||||
}
|
||||
if (options.signal?.aborted) {
|
||||
await fullStream.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
metrics.ai
|
||||
.counter('chat_object_stream_errors')
|
||||
.add(1, { model: model.id });
|
||||
throw this.handleError(e, model.id, options);
|
||||
}
|
||||
}
|
||||
|
||||
override async structure(
|
||||
cond: ModelConditions,
|
||||
messages: PromptMessage[],
|
||||
@@ -440,31 +414,6 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
}
|
||||
}
|
||||
|
||||
private async getFullStream(
|
||||
model: CopilotProviderModel,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotChatOptions = {}
|
||||
) {
|
||||
const [system, msgs] = await chatToGPTMessage(messages);
|
||||
const modelInstance = this.#instance.responses(model.id);
|
||||
const { fullStream } = streamText({
|
||||
model: modelInstance,
|
||||
system,
|
||||
messages: msgs,
|
||||
frequencyPenalty: options.frequencyPenalty ?? 0,
|
||||
presencePenalty: options.presencePenalty ?? 0,
|
||||
temperature: options.temperature ?? 0,
|
||||
maxTokens: options.maxTokens ?? 4096,
|
||||
providerOptions: {
|
||||
openai: this.getOpenAIOptions(options, model.id),
|
||||
},
|
||||
tools: await this.getTools(options, model.id),
|
||||
maxSteps: this.MAX_STEPS,
|
||||
abortSignal: options.signal,
|
||||
});
|
||||
return fullStream;
|
||||
}
|
||||
|
||||
// ====== text to image ======
|
||||
private async *generateImageWithAttachments(
|
||||
model: string,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { Tool, ToolSet } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
@@ -9,18 +7,9 @@ import {
|
||||
CopilotProviderNotSupported,
|
||||
OnEvent,
|
||||
} from '../../../base';
|
||||
import { AccessController } from '../../../core/permission';
|
||||
import { CopilotContextService } from '../context';
|
||||
import {
|
||||
buildDocSearchGetter,
|
||||
createDocSemanticSearchTool,
|
||||
createExaCrawlTool,
|
||||
createExaSearchTool,
|
||||
} from '../tools';
|
||||
import { CopilotProviderFactory } from './factory';
|
||||
import {
|
||||
type CopilotChatOptions,
|
||||
CopilotChatTools,
|
||||
type CopilotEmbeddingOptions,
|
||||
type CopilotImageOptions,
|
||||
CopilotProviderModel,
|
||||
@@ -33,7 +22,6 @@ import {
|
||||
ModelInputType,
|
||||
type PromptMessage,
|
||||
PromptMessageSchema,
|
||||
StreamObject,
|
||||
} from './types';
|
||||
|
||||
@Injectable()
|
||||
@@ -45,7 +33,6 @@ export abstract class CopilotProvider<C = any> {
|
||||
|
||||
@Inject() protected readonly AFFiNEConfig!: Config;
|
||||
@Inject() protected readonly factory!: CopilotProviderFactory;
|
||||
@Inject() protected readonly moduleRef!: ModuleRef;
|
||||
|
||||
get config(): C {
|
||||
return this.AFFiNEConfig.copilot.providers[this.type] as C;
|
||||
@@ -111,50 +98,6 @@ export abstract class CopilotProvider<C = any> {
|
||||
);
|
||||
}
|
||||
|
||||
protected getProviderSpecificTools(
|
||||
_toolName: CopilotChatTools,
|
||||
_model: string
|
||||
): [string, Tool] | undefined {
|
||||
return;
|
||||
}
|
||||
|
||||
// use for tool use, shared between providers
|
||||
protected async getTools(
|
||||
options: CopilotChatOptions,
|
||||
model: string
|
||||
): Promise<ToolSet> {
|
||||
const tools: ToolSet = {};
|
||||
if (options?.tools?.length) {
|
||||
for (const tool of options.tools) {
|
||||
const toolDef = this.getProviderSpecificTools(tool, model);
|
||||
if (toolDef) {
|
||||
tools[toolDef[0]] = toolDef[1];
|
||||
continue;
|
||||
}
|
||||
switch (tool) {
|
||||
case 'docSemanticSearch': {
|
||||
const ac = this.moduleRef.get(AccessController, { strict: false });
|
||||
const context = this.moduleRef.get(CopilotContextService, {
|
||||
strict: false,
|
||||
});
|
||||
const searchDocs = buildDocSearchGetter(ac, context);
|
||||
tools.doc_semantic_search = createDocSemanticSearchTool(
|
||||
searchDocs.bind(null, options)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'webSearch': {
|
||||
tools.web_search_exa = createExaSearchTool(this.AFFiNEConfig);
|
||||
tools.web_crawl_exa = createExaCrawlTool(this.AFFiNEConfig);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
private handleZodError(ret: z.SafeParseReturnType<any, any>) {
|
||||
if (ret.success) return;
|
||||
const issues = ret.error.issues.map(i => {
|
||||
@@ -226,17 +169,6 @@ export abstract class CopilotProvider<C = any> {
|
||||
options?: CopilotChatOptions
|
||||
): AsyncIterable<string>;
|
||||
|
||||
streamObject(
|
||||
_model: ModelConditions,
|
||||
_messages: PromptMessage[],
|
||||
_options?: CopilotChatOptions
|
||||
): AsyncIterable<StreamObject> {
|
||||
throw new CopilotProviderNotSupported({
|
||||
provider: this.type,
|
||||
kind: 'object',
|
||||
});
|
||||
}
|
||||
|
||||
structure(
|
||||
_cond: ModelConditions,
|
||||
_messages: PromptMessage[],
|
||||
|
||||
@@ -57,21 +57,7 @@ export const VertexSchema: JSONSchema = {
|
||||
// ========== prompt ==========
|
||||
|
||||
export const PromptConfigStrictSchema = z.object({
|
||||
tools: z
|
||||
.enum([
|
||||
// work with morph
|
||||
'docEdit',
|
||||
// work with indexer
|
||||
'docRead',
|
||||
'docKeywordSearch',
|
||||
// work with embeddings
|
||||
'docSemanticSearch',
|
||||
// work with exa/model internal tools
|
||||
'webSearch',
|
||||
])
|
||||
.array()
|
||||
.nullable()
|
||||
.optional(),
|
||||
tools: z.enum(['webSearch']).array().nullable().optional(),
|
||||
// params requirements
|
||||
requireContent: z.boolean().nullable().optional(),
|
||||
requireAttachment: z.boolean().nullable().optional(),
|
||||
@@ -118,33 +104,8 @@ export const ChatMessageAttachment = z.union([
|
||||
}),
|
||||
]);
|
||||
|
||||
export const StreamObjectSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('text-delta'),
|
||||
textDelta: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('reasoning'),
|
||||
textDelta: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('tool-call'),
|
||||
toolCallId: z.string(),
|
||||
toolName: z.string(),
|
||||
args: z.record(z.any()),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('tool-result'),
|
||||
toolCallId: z.string(),
|
||||
toolName: z.string(),
|
||||
args: z.record(z.any()),
|
||||
result: z.any(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const PureMessageSchema = z.object({
|
||||
content: z.string(),
|
||||
streamObjects: z.array(StreamObjectSchema).optional().nullable(),
|
||||
attachments: z.array(ChatMessageAttachment).optional().nullable(),
|
||||
params: z.record(z.any()).optional().nullable(),
|
||||
});
|
||||
@@ -154,14 +115,12 @@ export const PromptMessageSchema = PureMessageSchema.extend({
|
||||
}).strict();
|
||||
export type PromptMessage = z.infer<typeof PromptMessageSchema>;
|
||||
export type PromptParams = NonNullable<PromptMessage['params']>;
|
||||
export type StreamObject = z.infer<typeof StreamObjectSchema>;
|
||||
|
||||
// ========== options ==========
|
||||
|
||||
const CopilotProviderOptionsSchema = z.object({
|
||||
signal: z.instanceof(AbortSignal).optional(),
|
||||
user: z.string().optional(),
|
||||
workspace: z.string().optional(),
|
||||
});
|
||||
|
||||
export const CopilotChatOptionsSchema = CopilotProviderOptionsSchema.merge(
|
||||
@@ -174,9 +133,6 @@ export const CopilotChatOptionsSchema = CopilotProviderOptionsSchema.merge(
|
||||
.optional();
|
||||
|
||||
export type CopilotChatOptions = z.infer<typeof CopilotChatOptionsSchema>;
|
||||
export type CopilotChatTools = NonNullable<
|
||||
NonNullable<CopilotChatOptions>['tools']
|
||||
>[number];
|
||||
|
||||
export const CopilotStructuredOptionsSchema =
|
||||
CopilotProviderOptionsSchema.merge(PromptConfigStrictSchema).optional();
|
||||
@@ -213,7 +169,6 @@ export enum ModelInputType {
|
||||
|
||||
export enum ModelOutputType {
|
||||
Text = 'text',
|
||||
Object = 'object',
|
||||
Embedding = 'embedding',
|
||||
Image = 'image',
|
||||
Structured = 'structured',
|
||||
|
||||
@@ -9,12 +9,8 @@ import {
|
||||
} from 'ai';
|
||||
import { ZodType } from 'zod';
|
||||
|
||||
import {
|
||||
createDocSemanticSearchTool,
|
||||
createExaCrawlTool,
|
||||
createExaSearchTool,
|
||||
} from '../tools';
|
||||
import { PromptMessage, StreamObject } from './types';
|
||||
import { createExaCrawlTool, createExaSearchTool } from '../tools';
|
||||
import { PromptMessage } from './types';
|
||||
|
||||
type ChatMessage = CoreUserMessage | CoreAssistantMessage;
|
||||
|
||||
@@ -380,29 +376,12 @@ export class CitationParser {
|
||||
}
|
||||
|
||||
export interface CustomAITools extends ToolSet {
|
||||
doc_semantic_search: ReturnType<typeof createDocSemanticSearchTool>;
|
||||
web_search_exa: ReturnType<typeof createExaSearchTool>;
|
||||
web_crawl_exa: ReturnType<typeof createExaCrawlTool>;
|
||||
}
|
||||
|
||||
type ChunkType = TextStreamPart<CustomAITools>['type'];
|
||||
|
||||
export function parseUnknownError(error: unknown) {
|
||||
if (typeof error === 'string') {
|
||||
throw new Error(error);
|
||||
} else if (error instanceof Error) {
|
||||
throw error;
|
||||
} else if (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'message' in error
|
||||
) {
|
||||
throw new Error(String(error.message));
|
||||
} else {
|
||||
throw new Error(JSON.stringify(error));
|
||||
}
|
||||
}
|
||||
|
||||
export class TextStreamParser {
|
||||
private readonly CALLOUT_PREFIX = '\n[!]\n';
|
||||
|
||||
@@ -445,12 +424,6 @@ export class TextStreamParser {
|
||||
case 'tool-result': {
|
||||
result = this.addPrefix(result);
|
||||
switch (chunk.toolName) {
|
||||
case 'doc_semantic_search': {
|
||||
if (Array.isArray(chunk.result)) {
|
||||
result += `\nFound ${chunk.result.length} document${chunk.result.length !== 1 ? 's' : ''} related to “${chunk.args.query}”.\n`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'web_search_exa': {
|
||||
if (Array.isArray(chunk.result)) {
|
||||
result += `\n${this.getWebSearchLinks(chunk.result)}\n`;
|
||||
@@ -462,8 +435,8 @@ export class TextStreamParser {
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
parseUnknownError(chunk.error);
|
||||
break;
|
||||
const error = chunk.error as { type: string; message: string };
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
this.lastType = chunk.type;
|
||||
@@ -506,54 +479,3 @@ export class TextStreamParser {
|
||||
return links;
|
||||
}
|
||||
}
|
||||
|
||||
export class StreamObjectParser {
|
||||
public parse(chunk: TextStreamPart<CustomAITools>) {
|
||||
switch (chunk.type) {
|
||||
case 'reasoning':
|
||||
case 'text-delta':
|
||||
case 'tool-call':
|
||||
case 'tool-result': {
|
||||
return chunk;
|
||||
}
|
||||
case 'error': {
|
||||
parseUnknownError(chunk.error);
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public mergeTextDelta(chunks: StreamObject[]): StreamObject[] {
|
||||
return chunks.reduce((acc, curr) => {
|
||||
const prev = acc.at(-1);
|
||||
switch (curr.type) {
|
||||
case 'reasoning':
|
||||
case 'text-delta': {
|
||||
if (prev && prev.type === curr.type) {
|
||||
prev.textDelta += curr.textDelta;
|
||||
} else {
|
||||
acc.push(curr);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
acc.push(curr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as StreamObject[]);
|
||||
}
|
||||
|
||||
public mergeContent(chunks: StreamObject[]): string {
|
||||
return chunks.reduce((acc, curr) => {
|
||||
if (curr.type === 'text-delta') {
|
||||
acc += curr.textDelta;
|
||||
}
|
||||
return acc;
|
||||
}, '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import { Admin } from '../../core/common';
|
||||
import { AccessController } from '../../core/permission';
|
||||
import { UserType } from '../../core/user';
|
||||
import { PromptService } from './prompt';
|
||||
import { PromptMessage, StreamObject } from './providers';
|
||||
import { PromptMessage } from './providers';
|
||||
import { ChatSessionService } from './session';
|
||||
import { CopilotStorage } from './storage';
|
||||
import {
|
||||
@@ -168,27 +168,6 @@ class QueryChatHistoriesInput implements Partial<ListHistoriesOptions> {
|
||||
|
||||
// ================== Return Types ==================
|
||||
|
||||
@ObjectType('StreamObject')
|
||||
class StreamObjectType {
|
||||
@Field(() => String)
|
||||
type!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
textDelta?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
toolCallId?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
toolName?: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
args?: any;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
result?: any;
|
||||
}
|
||||
|
||||
@ObjectType('ChatMessage')
|
||||
class ChatMessageType implements Partial<ChatMessage> {
|
||||
// id will be null if message is a prompt message
|
||||
@@ -201,9 +180,6 @@ class ChatMessageType implements Partial<ChatMessage> {
|
||||
@Field(() => String)
|
||||
content!: string;
|
||||
|
||||
@Field(() => [StreamObjectType], { nullable: true })
|
||||
streamObjects!: StreamObject[];
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
attachments!: string[];
|
||||
|
||||
|
||||
@@ -282,7 +282,6 @@ export class ChatSessionService {
|
||||
await tx.aiSessionMessage.createMany({
|
||||
data: state.messages.map(m => ({
|
||||
...m,
|
||||
streamObjects: m.streamObjects || undefined,
|
||||
attachments: m.attachments || undefined,
|
||||
params: omit(m.params, ['docs']) || undefined,
|
||||
sessionId,
|
||||
@@ -513,7 +512,6 @@ export class ChatSessionService {
|
||||
id: true,
|
||||
role: true,
|
||||
content: true,
|
||||
streamObjects: true,
|
||||
attachments: true,
|
||||
params: true,
|
||||
createdAt: true,
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { AccessController } from '../../../core/permission';
|
||||
import type { ChunkSimilarity } from '../../../models';
|
||||
import type { CopilotContextService } from '../context';
|
||||
import type { CopilotChatOptions } from '../providers';
|
||||
|
||||
export const buildDocSearchGetter = (
|
||||
ac: AccessController,
|
||||
context: CopilotContextService
|
||||
) => {
|
||||
const searchDocs = async (options: CopilotChatOptions, query?: string) => {
|
||||
if (!options || !query?.trim() || !options.user || !options.workspace) {
|
||||
return undefined;
|
||||
}
|
||||
const canAccess = await ac
|
||||
.user(options.user)
|
||||
.workspace(options.workspace)
|
||||
.can('Workspace.Read');
|
||||
if (!canAccess) return undefined;
|
||||
const chunks = await context.matchWorkspaceAll(options.workspace, query);
|
||||
const docChunks = await ac
|
||||
.user(options.user)
|
||||
.workspace(options.workspace)
|
||||
.docs(
|
||||
chunks.filter(c => 'docId' in c),
|
||||
'Doc.Read'
|
||||
);
|
||||
const fileChunks = chunks.filter(c => 'fileId' in c);
|
||||
if (!docChunks.length && !fileChunks.length) return undefined;
|
||||
return [...fileChunks, ...docChunks];
|
||||
};
|
||||
return searchDocs;
|
||||
};
|
||||
|
||||
export const createDocSemanticSearchTool = (
|
||||
searchDocs: (query: string) => Promise<ChunkSimilarity[] | undefined>
|
||||
) => {
|
||||
return tool({
|
||||
description:
|
||||
'Semantic search for relevant documents in the current workspace',
|
||||
parameters: z.object({
|
||||
query: z.string().describe('The query to search for.'),
|
||||
}),
|
||||
execute: async ({ query }) => {
|
||||
try {
|
||||
return await searchDocs(query);
|
||||
} catch {
|
||||
return 'Failed to search documents.';
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,2 +1 @@
|
||||
export * from './doc-semantic-search';
|
||||
export * from './web-search';
|
||||
|
||||
-27
@@ -494,30 +494,3 @@ Generated by [AVA](https://avajs.dev).
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
## should search blob names from doc snapshot work
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
Map {
|
||||
'ldZMrM4PDlsNG4Q4YvCsz623h6TKu4qI9_FpTqIypfw=' => 'test file name here.txt',
|
||||
}
|
||||
|
||||
## should search blob names work
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
[
|
||||
[
|
||||
'blob1',
|
||||
'blob1 name.txt',
|
||||
],
|
||||
[
|
||||
'blob2',
|
||||
'blob2 name.md',
|
||||
],
|
||||
[
|
||||
'blob3',
|
||||
'blob3 name.docx',
|
||||
],
|
||||
]
|
||||
|
||||
BIN
Binary file not shown.
@@ -2113,103 +2113,3 @@ test('should index doc work', async t => {
|
||||
t.is(module.event.count('doc.indexer.updated'), count + 1);
|
||||
});
|
||||
// #endregion
|
||||
|
||||
// #region searchBlobNames()
|
||||
|
||||
test('should search blob names from doc snapshot work', async t => {
|
||||
const docSnapshot = await module.create(Mockers.DocSnapshot, {
|
||||
workspaceId: workspace.id,
|
||||
user,
|
||||
snapshotFile: 'test-doc-with-blob.snapshot.bin',
|
||||
});
|
||||
|
||||
await indexerService.indexDoc(workspace.id, docSnapshot.id, {
|
||||
refresh: true,
|
||||
});
|
||||
|
||||
const blobNameMap = await indexerService.searchBlobNames(workspace.id, [
|
||||
'ldZMrM4PDlsNG4Q4YvCsz623h6TKu4qI9_FpTqIypfw=',
|
||||
]);
|
||||
|
||||
t.snapshot(blobNameMap);
|
||||
});
|
||||
|
||||
test('should search blob names work', async t => {
|
||||
const workspaceId = randomUUID();
|
||||
const blobId1 = 'blob1';
|
||||
const blobId2 = 'blob2';
|
||||
const blobId3 = 'blob3';
|
||||
const blobId4 = 'blob4';
|
||||
|
||||
await indexerService.write(
|
||||
SearchTable.block,
|
||||
[
|
||||
{
|
||||
workspaceId,
|
||||
blob: blobId1,
|
||||
content: 'blob1 name.txt',
|
||||
flavour: 'affine:attachment',
|
||||
docId: randomUUID(),
|
||||
blockId: randomUUID(),
|
||||
createdByUserId: user.id,
|
||||
updatedByUserId: user.id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
workspaceId,
|
||||
blob: blobId2,
|
||||
content: 'blob2 name.md',
|
||||
flavour: 'affine:attachment',
|
||||
docId: randomUUID(),
|
||||
blockId: randomUUID(),
|
||||
createdByUserId: user.id,
|
||||
updatedByUserId: user.id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
workspaceId,
|
||||
blob: blobId3,
|
||||
content: 'blob3 name.docx',
|
||||
flavour: 'affine:attachment',
|
||||
docId: randomUUID(),
|
||||
blockId: randomUUID(),
|
||||
createdByUserId: user.id,
|
||||
updatedByUserId: user.id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
// no attachment
|
||||
{
|
||||
workspaceId,
|
||||
blob: blobId3,
|
||||
content: 'mock blob3 content',
|
||||
flavour: 'affine:page',
|
||||
docId: randomUUID(),
|
||||
blockId: randomUUID(),
|
||||
createdByUserId: user.id,
|
||||
updatedByUserId: user.id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
],
|
||||
{
|
||||
refresh: true,
|
||||
}
|
||||
);
|
||||
|
||||
const blobNameMap = await indexerService.searchBlobNames(workspaceId, [
|
||||
blobId1,
|
||||
blobId2,
|
||||
blobId3,
|
||||
blobId4,
|
||||
]);
|
||||
|
||||
t.is(blobNameMap.size, 3);
|
||||
t.snapshot(
|
||||
Array.from(blobNameMap.entries()).sort((a, b) => a[0].localeCompare(b[0]))
|
||||
);
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
@@ -387,52 +387,6 @@ export class IndexerService {
|
||||
await searchProvider.deleteByQuery(table, dsl, options);
|
||||
}
|
||||
|
||||
async searchBlobNames(workspaceId: string, blobIds: string[]) {
|
||||
const result = await this.search({
|
||||
table: SearchTable.block,
|
||||
query: {
|
||||
type: SearchQueryType.boolean,
|
||||
occur: SearchQueryOccur.must,
|
||||
queries: [
|
||||
{
|
||||
type: SearchQueryType.match,
|
||||
field: 'workspaceId',
|
||||
match: workspaceId,
|
||||
},
|
||||
{
|
||||
type: SearchQueryType.match,
|
||||
field: 'flavour',
|
||||
match: 'affine:attachment',
|
||||
},
|
||||
{
|
||||
type: SearchQueryType.boolean,
|
||||
occur: SearchQueryOccur.should,
|
||||
queries: blobIds.map(blobId => ({
|
||||
type: SearchQueryType.match,
|
||||
field: 'blob',
|
||||
match: blobId,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
fields: ['blob', 'content'],
|
||||
pagination: {
|
||||
limit: 10000,
|
||||
},
|
||||
},
|
||||
});
|
||||
const blobNameMap = new Map<string, string>();
|
||||
for (const node of result.nodes) {
|
||||
const blobId = node.fields.blob[0] as string;
|
||||
const content = node.fields.content[0] as string;
|
||||
if (blobId && content) {
|
||||
blobNameMap.set(blobId, content);
|
||||
}
|
||||
}
|
||||
return blobNameMap;
|
||||
}
|
||||
|
||||
#formatSearchNodes(nodes: SearchNode[]) {
|
||||
return nodes.map(node => ({
|
||||
...node,
|
||||
|
||||
@@ -96,7 +96,6 @@ type ChatMessage {
|
||||
id: ID
|
||||
params: JSON
|
||||
role: String!
|
||||
streamObjects: [StreamObject!]
|
||||
}
|
||||
|
||||
enum ContextCategories {
|
||||
@@ -1629,15 +1628,6 @@ type SpaceShouldHaveOnlyOneOwnerDataType {
|
||||
spaceId: String!
|
||||
}
|
||||
|
||||
type StreamObject {
|
||||
args: JSON
|
||||
result: JSON
|
||||
textDelta: String
|
||||
toolCallId: String
|
||||
toolName: String
|
||||
type: String!
|
||||
}
|
||||
|
||||
type SubscriptionAlreadyExistsDataType {
|
||||
plan: String!
|
||||
}
|
||||
|
||||
@@ -14,14 +14,6 @@ query getCopilotHistories(
|
||||
id
|
||||
role
|
||||
content
|
||||
streamObjects {
|
||||
type
|
||||
textDelta
|
||||
toolCallId
|
||||
toolName
|
||||
args
|
||||
result
|
||||
}
|
||||
attachments
|
||||
createdAt
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const passwordLimitsFragment = `fragment PasswordLimits on PasswordLimits
|
||||
minLength
|
||||
maxLength
|
||||
}`;
|
||||
export const licenseBodyFragment = `fragment licenseBody on License {
|
||||
export const licenseFragment = `fragment license on License {
|
||||
expiredAt
|
||||
installedAt
|
||||
quantity
|
||||
@@ -617,14 +617,6 @@ export const getCopilotHistoriesQuery = {
|
||||
id
|
||||
role
|
||||
content
|
||||
streamObjects {
|
||||
type
|
||||
textDelta
|
||||
toolCallId
|
||||
toolName
|
||||
args
|
||||
result
|
||||
}
|
||||
attachments
|
||||
createdAt
|
||||
}
|
||||
@@ -1451,10 +1443,10 @@ export const activateLicenseMutation = {
|
||||
op: 'activateLicense',
|
||||
query: `mutation activateLicense($workspaceId: String!, $license: String!) {
|
||||
activateLicense(workspaceId: $workspaceId, license: $license) {
|
||||
...licenseBody
|
||||
...license
|
||||
}
|
||||
}
|
||||
${licenseBodyFragment}`,
|
||||
${licenseFragment}`,
|
||||
};
|
||||
|
||||
export const deactivateLicenseMutation = {
|
||||
@@ -1471,11 +1463,11 @@ export const getLicenseQuery = {
|
||||
query: `query getLicense($workspaceId: String!) {
|
||||
workspace(id: $workspaceId) {
|
||||
license {
|
||||
...licenseBody
|
||||
...license
|
||||
}
|
||||
}
|
||||
}
|
||||
${licenseBodyFragment}`,
|
||||
${licenseFragment}`,
|
||||
};
|
||||
|
||||
export const installLicenseMutation = {
|
||||
@@ -1483,10 +1475,10 @@ export const installLicenseMutation = {
|
||||
op: 'installLicense',
|
||||
query: `mutation installLicense($workspaceId: String!, $license: Upload!) {
|
||||
installLicense(workspaceId: $workspaceId, license: $license) {
|
||||
...licenseBody
|
||||
...license
|
||||
}
|
||||
}
|
||||
${licenseBodyFragment}`,
|
||||
${licenseFragment}`,
|
||||
file: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#import './license-body.gql'
|
||||
#import './license.gql'
|
||||
|
||||
mutation activateLicense($workspaceId: String!, $license: String!) {
|
||||
activateLicense(workspaceId: $workspaceId, license: $license) {
|
||||
...licenseBody
|
||||
...license
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
#import './license-body.gql'
|
||||
#import './license.gql'
|
||||
|
||||
query getLicense($workspaceId: String!) {
|
||||
workspace(id: $workspaceId) {
|
||||
license {
|
||||
...licenseBody
|
||||
...license
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#import './license-body.gql'
|
||||
#import './license.gql'
|
||||
|
||||
mutation installLicense($workspaceId: String!, $license: Upload!) {
|
||||
installLicense(workspaceId: $workspaceId, license: $license) {
|
||||
...licenseBody
|
||||
...license
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
fragment licenseBody on License {
|
||||
fragment license on License {
|
||||
expiredAt
|
||||
installedAt
|
||||
quantity
|
||||
@@ -137,7 +137,6 @@ export interface ChatMessage {
|
||||
id: Maybe<Scalars['ID']['output']>;
|
||||
params: Maybe<Scalars['JSON']['output']>;
|
||||
role: Scalars['String']['output'];
|
||||
streamObjects: Maybe<Array<StreamObject>>;
|
||||
}
|
||||
|
||||
export enum ContextCategories {
|
||||
@@ -2196,16 +2195,6 @@ export interface SpaceShouldHaveOnlyOneOwnerDataType {
|
||||
spaceId: Scalars['String']['output'];
|
||||
}
|
||||
|
||||
export interface StreamObject {
|
||||
__typename?: 'StreamObject';
|
||||
args: Maybe<Scalars['JSON']['output']>;
|
||||
result: Maybe<Scalars['JSON']['output']>;
|
||||
textDelta: Maybe<Scalars['String']['output']>;
|
||||
toolCallId: Maybe<Scalars['String']['output']>;
|
||||
toolName: Maybe<Scalars['String']['output']>;
|
||||
type: Scalars['String']['output'];
|
||||
}
|
||||
|
||||
export interface SubscriptionAlreadyExistsDataType {
|
||||
__typename?: 'SubscriptionAlreadyExistsDataType';
|
||||
plan: Scalars['String']['output'];
|
||||
@@ -3385,15 +3374,6 @@ export type GetCopilotHistoriesQuery = {
|
||||
content: string;
|
||||
attachments: Array<string> | null;
|
||||
createdAt: string;
|
||||
streamObjects: Array<{
|
||||
__typename?: 'StreamObject';
|
||||
type: string;
|
||||
textDelta: string | null;
|
||||
toolCallId: string | null;
|
||||
toolName: string | null;
|
||||
args: Record<string, string> | null;
|
||||
result: Record<string, string> | null;
|
||||
}> | null;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
@@ -4421,7 +4401,7 @@ export type InstallLicenseMutation = {
|
||||
};
|
||||
};
|
||||
|
||||
export type LicenseBodyFragment = {
|
||||
export type LicenseFragment = {
|
||||
__typename?: 'License';
|
||||
expiredAt: string | null;
|
||||
installedAt: string;
|
||||
|
||||
@@ -16,7 +16,6 @@ export type AppSetting = {
|
||||
autoCheckUpdate: boolean;
|
||||
autoDownloadUpdate: boolean;
|
||||
enableTelemetry: boolean;
|
||||
showLinkedDocInSidebar: boolean;
|
||||
};
|
||||
export const windowFrameStyleOptions: AppSetting['windowFrameStyle'][] = [
|
||||
'frameless',
|
||||
@@ -34,7 +33,6 @@ const appSettingBaseAtom = atomWithStorage<AppSetting>(
|
||||
autoCheckUpdate: true,
|
||||
autoDownloadUpdate: true,
|
||||
enableTelemetry: true,
|
||||
showLinkedDocInSidebar: true,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
|
||||
@@ -52,509 +52,6 @@ exports[`should get all docs from root doc work 2`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`should parse page doc work 1`] = `
|
||||
{
|
||||
"md": "AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# You own your data, with no compromises
|
||||
|
||||
|
||||
## Local-first & Real-time collaborative
|
||||
|
||||
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.
|
||||
|
||||
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard
|
||||
|
||||
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.
|
||||
|
||||
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.
|
||||
|
||||
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:
|
||||
|
||||
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.
|
||||
|
||||
|
||||
## A true canvas for blocks in any form
|
||||
|
||||
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:
|
||||
|
||||
|
||||
* Quip & Notion with their great concept of "everything is a block"
|
||||
|
||||
|
||||
* Trello with their Kanban
|
||||
|
||||
|
||||
* Airtable & Miro with their no-code programable datasheets
|
||||
|
||||
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard
|
||||
|
||||
|
||||
* Remnote & Capacities with their object-based tag system
|
||||
|
||||
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)
|
||||
|
||||
|
||||
## Self Host
|
||||
|
||||
|
||||
Self host AFFiNE
|
||||
|
||||
|
||||
||Title|Tag|
|
||||
|---|---|---|
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|
|
||||
|For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>|
|
||||
|Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|
|
||||
|Trello with their Kanban|Trello with their Kanban|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|
|
||||
|Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||
|
||||
|
||||
|
||||
## Affine Development
|
||||
|
||||
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
",
|
||||
"parsedBlock": {
|
||||
"children": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"content": "AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "FoPQcAyV_m",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "oz48nn_zp8",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "# You own your data, with no compromises
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "g8a-D9-jXS",
|
||||
"type": "h1",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "## Local-first & Real-time collaborative
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "J8lHN1GR_5",
|
||||
"type": "h2",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "xCuWdM0VLz",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "zElMi0tViK",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "Z4rK0OF9Wk",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"content": "",
|
||||
"flavour": "affine:note",
|
||||
"id": "RX4CG2zsBk",
|
||||
"type": undefined,
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"content": "### Blocks that assemble your next docs, tasks kanban or whiteboard
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "DQ0Ryb-SpW",
|
||||
"type": "h3",
|
||||
},
|
||||
],
|
||||
"content": "",
|
||||
"flavour": "affine:note",
|
||||
"id": "S1mkc8zUoU",
|
||||
"type": undefined,
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"content": "There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "HAZC3URZp_",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "0H87ypiuv8",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "If you want to learn more about the product design of AFFiNE, here goes the concepts:
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "Sp4G1KD0Wn",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "RsUhDuEqXa",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"content": "",
|
||||
"flavour": "affine:note",
|
||||
"id": "yGlBdshAqN",
|
||||
"type": undefined,
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"content": "## A true canvas for blocks in any form
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "Z2HibKzAr-",
|
||||
"type": "h2",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "UwvWddamzM",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "g9xKUjhJj1",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": ""We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "wDTn4YJ4pm",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "* Quip & Notion with their great concept of "everything is a block"
|
||||
|
||||
",
|
||||
"flavour": "affine:list",
|
||||
"id": "xFrrdiP3-V",
|
||||
"type": "bulleted",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "* Trello with their Kanban
|
||||
|
||||
",
|
||||
"flavour": "affine:list",
|
||||
"id": "Tp9xyN4Okl",
|
||||
"type": "bulleted",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "* Airtable & Miro with their no-code programable datasheets
|
||||
|
||||
",
|
||||
"flavour": "affine:list",
|
||||
"id": "K_4hUzKZFQ",
|
||||
"type": "bulleted",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "* Miro & Whimiscal with their edgeless visual whiteboard
|
||||
|
||||
",
|
||||
"flavour": "affine:list",
|
||||
"id": "QwMzON2s7x",
|
||||
"type": "bulleted",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "* Remnote & Capacities with their object-based tag system
|
||||
|
||||
",
|
||||
"flavour": "affine:list",
|
||||
"id": "FFVmit6u1T",
|
||||
"type": "bulleted",
|
||||
},
|
||||
],
|
||||
"content": "",
|
||||
"flavour": "affine:note",
|
||||
"id": "6lDiuDqZGL",
|
||||
"type": undefined,
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"content": "For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "YqnG5O6AE6",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "## Self Host
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "sbDTmZMZcq",
|
||||
"type": "h2",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "Self host AFFiNE
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "QVvitesfbj",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"content": "",
|
||||
"flavour": "affine:note",
|
||||
"id": "cauvaHOQmh",
|
||||
"type": undefined,
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"content": "||Title|Tag|
|
||||
|---|---|---|
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|
|
||||
|For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>|
|
||||
|Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|
|
||||
|Trello with their Kanban|Trello with their Kanban|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|
|
||||
|Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||
|
||||
|
||||
",
|
||||
"flavour": "affine:database",
|
||||
"id": "U_GoHFD9At",
|
||||
"rows": [
|
||||
{
|
||||
"Tag": "<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>",
|
||||
"Title": "Affine Development
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Affine Development
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>",
|
||||
"Title": "For developers or installations guides, please go to AFFiNE Doc
|
||||
|
||||
|
||||
",
|
||||
"undefined": "For developers or installations guides, please go to AFFiNE Doc
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
|
||||
"Title": "Quip & Notion with their great concept of "everything is a block"
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Quip & Notion with their great concept of "everything is a block"
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
|
||||
"Title": "Trello with their Kanban
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Trello with their Kanban
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
|
||||
"Title": "Airtable & Miro with their no-code programable datasheets
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Airtable & Miro with their no-code programable datasheets
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
|
||||
"Title": "Miro & Whimiscal with their edgeless visual whiteboard
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Miro & Whimiscal with their edgeless visual whiteboard
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "",
|
||||
"Title": "Remnote & Capacities with their object-based tag system
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Remnote & Capacities with their object-based tag system
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
],
|
||||
"title": "Learning From",
|
||||
"type": undefined,
|
||||
},
|
||||
],
|
||||
"content": "",
|
||||
"flavour": "affine:note",
|
||||
"id": "2jwCeO8Yot",
|
||||
"type": undefined,
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"content": "## Affine Development
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "NyHXrMX3R1",
|
||||
"type": "h2",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "9-K49otbCv",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"content": "
|
||||
|
||||
",
|
||||
"flavour": "affine:paragraph",
|
||||
"id": "faFteK9eG-",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"content": "",
|
||||
"flavour": "affine:note",
|
||||
"id": "c9MF_JiRgx",
|
||||
"type": undefined,
|
||||
},
|
||||
],
|
||||
"content": "",
|
||||
"flavour": "affine:page",
|
||||
"id": "TnUgtVg7Eu",
|
||||
"type": undefined,
|
||||
},
|
||||
"title": "Write, Draw, Plan all at Once.",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`should read all doc ids from root doc snapshot work 1`] = `
|
||||
[
|
||||
"5nS9BSp3Px",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { expect, test } from 'vitest';
|
||||
import { applyUpdate, Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
|
||||
|
||||
import {
|
||||
parsePageDoc,
|
||||
readAllBlocksFromDoc,
|
||||
readAllDocIdsFromRootDoc,
|
||||
readAllDocsFromRootDoc,
|
||||
@@ -101,20 +100,3 @@ test('should read all doc ids from root doc snapshot work', async () => {
|
||||
const docIds = readAllDocIdsFromRootDoc(rootDoc);
|
||||
expect(docIds).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should parse page doc work', () => {
|
||||
const doc = new YDoc({
|
||||
guid: 'test-doc',
|
||||
});
|
||||
applyUpdate(doc, docSnapshot);
|
||||
|
||||
const result = parsePageDoc({
|
||||
workspaceId: 'test-space',
|
||||
doc,
|
||||
buildBlobUrl: id => `blob://${id}`,
|
||||
buildDocUrl: id => `doc://${id}`,
|
||||
renderDocTitle: id => `Doc Title ${id}`,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
A fork of https://github.com/frysztak/quill-delta-to-markdown
|
||||
@@ -1,95 +0,0 @@
|
||||
// eslint-disable
|
||||
// @ts-nocheck
|
||||
import { Node } from './utils/node';
|
||||
import { encodeLink } from './utils/url';
|
||||
|
||||
export interface InlineReference {
|
||||
type: 'LinkedPage';
|
||||
pageId: string;
|
||||
title?: string;
|
||||
params?: { mode: 'doc' | 'edgeless' };
|
||||
}
|
||||
|
||||
export interface ConverterOptions {
|
||||
convertInlineReferenceLink?: (reference: InlineReference) => {
|
||||
title: string;
|
||||
link: string;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultConvertInlineReferenceLink = (reference: InlineReference) => {
|
||||
return {
|
||||
title: reference.title || '',
|
||||
link: [reference.type, reference.pageId, reference.params?.mode]
|
||||
.filter(Boolean)
|
||||
.join(':'),
|
||||
};
|
||||
};
|
||||
|
||||
export function getConverters(opts: ConverterOptions = {}) {
|
||||
const { convertInlineReferenceLink = defaultConvertInlineReferenceLink } =
|
||||
opts;
|
||||
|
||||
return {
|
||||
embed: {
|
||||
image: function (src) {
|
||||
this.append(' + ')');
|
||||
},
|
||||
// Not a default Quill feature, converts custom divider embed blot added when
|
||||
// creating quill editor instance.
|
||||
// See https://quilljs.com/guides/cloning-medium-with-parchment/#dividers
|
||||
thematic_break: function () {
|
||||
this.open = '\n---\n' + this.open;
|
||||
},
|
||||
},
|
||||
|
||||
inline: {
|
||||
italic: function () {
|
||||
return ['_', '_'];
|
||||
},
|
||||
bold: function () {
|
||||
return ['**', '**'];
|
||||
},
|
||||
link: function (url) {
|
||||
return ['[', '](' + url + ')'];
|
||||
},
|
||||
reference: function (reference: InlineReference) {
|
||||
const { title, link } = convertInlineReferenceLink(reference);
|
||||
return ['[', `${title}](${link})`];
|
||||
},
|
||||
strike: function () {
|
||||
return ['~~', '~~'];
|
||||
},
|
||||
code: function () {
|
||||
return ['`', '`'];
|
||||
},
|
||||
},
|
||||
|
||||
block: {
|
||||
header: function ({ header }) {
|
||||
this.open = '#'.repeat(header) + ' ' + this.open;
|
||||
},
|
||||
blockquote: function () {
|
||||
this.open = '> ' + this.open;
|
||||
},
|
||||
list: {
|
||||
group: function () {
|
||||
return new Node(['', '\n']);
|
||||
},
|
||||
line: function (attrs, group) {
|
||||
if (attrs.list === 'bullet') {
|
||||
this.open = '- ' + this.open;
|
||||
} else if (attrs.list === 'checked') {
|
||||
this.open = '- [x] ' + this.open;
|
||||
} else if (attrs.list === 'unchecked') {
|
||||
this.open = '- [ ] ' + this.open;
|
||||
} else if (attrs.list === 'ordered') {
|
||||
group.count = group.count || 0;
|
||||
var count = ++group.count;
|
||||
this.open = count + '. ' + this.open;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
// eslint-disable
|
||||
// @ts-nocheck
|
||||
import { Node } from './utils/node';
|
||||
|
||||
export const deltaToMd = (delta, converters) => {
|
||||
return convert(delta, converters).render().trimEnd() + '\n';
|
||||
};
|
||||
|
||||
function convert(ops, converters) {
|
||||
let group, line, el, activeInline, beginningOfLine;
|
||||
let root = new Node();
|
||||
|
||||
function newLine() {
|
||||
el = line = new Node(['', '\n']);
|
||||
root.append(line);
|
||||
activeInline = {};
|
||||
}
|
||||
newLine();
|
||||
|
||||
for (let i = 0; i < ops.length; i++) {
|
||||
let op = ops[i];
|
||||
|
||||
if (typeof op.insert === 'object') {
|
||||
for (let k in op.insert) {
|
||||
if (converters.embed[k]) {
|
||||
applyInlineAttributes(op.attributes);
|
||||
converters.embed[k].call(el, op.insert[k], op.attributes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let lines = op.insert.split('\n');
|
||||
|
||||
if (hasBlockLevelAttribute(op.attributes, converters)) {
|
||||
// Some line-level styling (ie headings) is applied by inserting a \n
|
||||
// with the style; the style applies back to the previous \n.
|
||||
// There *should* only be one style in an insert operation.
|
||||
|
||||
for (let j = 1; j < lines.length; j++) {
|
||||
for (let attr in op.attributes) {
|
||||
if (converters.block[attr]) {
|
||||
let fn = converters.block[attr];
|
||||
if (typeof fn === 'object') {
|
||||
if (group && group.type !== attr) {
|
||||
group = null;
|
||||
}
|
||||
if (!group && fn.group) {
|
||||
group = {
|
||||
el: fn.group(),
|
||||
type: attr,
|
||||
value: op.attributes[attr],
|
||||
distance: 0,
|
||||
};
|
||||
root.append(group.el);
|
||||
}
|
||||
|
||||
if (group) {
|
||||
group.el.append(line);
|
||||
group.distance = 0;
|
||||
}
|
||||
fn = fn.line;
|
||||
}
|
||||
|
||||
fn.call(line, op.attributes, group);
|
||||
newLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
beginningOfLine = true;
|
||||
} else {
|
||||
for (let l = 0; l < lines.length; l++) {
|
||||
if ((l > 0 || beginningOfLine) && group && ++group.distance >= 2) {
|
||||
group = null;
|
||||
}
|
||||
applyInlineAttributes(
|
||||
op.attributes,
|
||||
ops[i + 1] && ops[i + 1].attributes
|
||||
);
|
||||
el.append(lines[l]);
|
||||
if (l < lines.length - 1) {
|
||||
newLine();
|
||||
}
|
||||
}
|
||||
beginningOfLine = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
|
||||
function applyInlineAttributes(attrs, next?: any) {
|
||||
let first: any[] = [];
|
||||
let then: any[] = [];
|
||||
attrs = attrs || {};
|
||||
|
||||
let tag = el,
|
||||
seen = {};
|
||||
while (tag._format) {
|
||||
seen[tag._format] = true;
|
||||
if (!attrs[tag._format] || tag.open !== tag.close) {
|
||||
for (let k in seen) {
|
||||
delete activeInline[k];
|
||||
}
|
||||
el = tag.parent();
|
||||
}
|
||||
|
||||
tag = tag.parent();
|
||||
}
|
||||
|
||||
for (let attr in attrs) {
|
||||
if (converters.inline[attr] && attrs[attr]) {
|
||||
if (activeInline[attr] && activeInline[attr] === attrs[attr]) {
|
||||
continue; // do nothing -- we should already be inside this style's tag
|
||||
}
|
||||
|
||||
if (next && attrs[attr] === next[attr]) {
|
||||
first.push(attr); // if the next operation has the same style, this should be the outermost tag
|
||||
} else {
|
||||
then.push(attr);
|
||||
}
|
||||
activeInline[attr] = attrs[attr];
|
||||
}
|
||||
}
|
||||
|
||||
first.forEach(apply);
|
||||
then.forEach(apply);
|
||||
|
||||
function apply(fmt) {
|
||||
let newEl = converters.inline[fmt].call(null, attrs[fmt]);
|
||||
if (Array.isArray(newEl)) {
|
||||
newEl = new Node(newEl);
|
||||
}
|
||||
newEl._format = fmt;
|
||||
el.append(newEl);
|
||||
el = newEl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasBlockLevelAttribute(attrs, converters) {
|
||||
for (let k in attrs) {
|
||||
if (Object.keys(converters.block).includes(k)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { getConverters } from './delta-converters';
|
||||
export { deltaToMd } from './delta-to-md';
|
||||
@@ -1,66 +0,0 @@
|
||||
// eslint-disable
|
||||
// @ts-nocheck
|
||||
let id = 0;
|
||||
|
||||
export class Node {
|
||||
id = ++id;
|
||||
children: Node[];
|
||||
open: string;
|
||||
close: string;
|
||||
text: string;
|
||||
|
||||
_format: string;
|
||||
_parent: Node;
|
||||
|
||||
constructor(data?: string[] | string) {
|
||||
if (Array.isArray(data)) {
|
||||
this.open = data[0];
|
||||
this.close = data[1];
|
||||
} else if (typeof data === 'string') {
|
||||
this.text = data;
|
||||
}
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
append(e: Node) {
|
||||
if (!(e instanceof Node)) {
|
||||
e = new Node(e);
|
||||
}
|
||||
if (e._parent) {
|
||||
const idx = e._parent.children.indexOf(e);
|
||||
e._parent.children.splice(idx, 1);
|
||||
}
|
||||
e._parent = this;
|
||||
this.children = this.children.concat(e);
|
||||
}
|
||||
|
||||
render() {
|
||||
const inner =
|
||||
(this.text || '') + this.children.map(c => c.render()).join('');
|
||||
|
||||
if (
|
||||
inner.trim() === '' &&
|
||||
this.open === this.close &&
|
||||
this.open &&
|
||||
this.close
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const wrapped = this.open && this.close;
|
||||
const emptyInner = inner.trim() === '';
|
||||
const fragments = [
|
||||
inner.startsWith(' ') && !emptyInner && wrapped ? ' ' : '',
|
||||
this.open,
|
||||
wrapped ? inner.trim() : inner,
|
||||
this.close,
|
||||
inner.endsWith(' ') && !emptyInner && wrapped ? ' ' : '',
|
||||
].filter(f => f);
|
||||
|
||||
return fragments.join('');
|
||||
}
|
||||
|
||||
parent() {
|
||||
return this._parent;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export const encodeLink = (link: string) =>
|
||||
encodeURI(link)
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/(\?|&)response-content-disposition=attachment.*$/, '');
|
||||
@@ -1,437 +0,0 @@
|
||||
import type { ColumnDataType } from '@blocksuite/affine/model';
|
||||
import { Array as YArray, type Map as YMap, type Text as YText } from 'yjs';
|
||||
|
||||
import { deltaToMd, getConverters } from './delta-to-md';
|
||||
import type {
|
||||
BaseParsedBlock,
|
||||
Flavour,
|
||||
ParsedBlock,
|
||||
ParsedDoc,
|
||||
ParserContext,
|
||||
SerializedCells,
|
||||
YBlock,
|
||||
YBlocks,
|
||||
} from './types';
|
||||
|
||||
export const parseBlockToMd = (
|
||||
block: BaseParsedBlock,
|
||||
padding = ''
|
||||
): string => {
|
||||
if (block.content) {
|
||||
return (
|
||||
block.content
|
||||
.split('\n')
|
||||
.map(line => padding + line)
|
||||
.join('\n') +
|
||||
'\n' +
|
||||
block.children.map(b => parseBlockToMd(b, padding + ' ')).join('')
|
||||
);
|
||||
} else {
|
||||
return block.children.map(b => parseBlockToMd(b, padding)).join('');
|
||||
}
|
||||
};
|
||||
|
||||
export function parseBlock(
|
||||
context: ParserContext,
|
||||
yBlock: YBlock | undefined,
|
||||
yBlocks: YBlocks // all blocks
|
||||
): ParsedBlock | null {
|
||||
if (!yBlock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deltaConverters = getConverters({
|
||||
convertInlineReferenceLink: ref => {
|
||||
return {
|
||||
title: ref.title || context.renderDocTitle?.(ref.pageId) || '',
|
||||
link: context.buildDocUrl(ref.pageId),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const id = yBlock.get('sys:id') as string;
|
||||
const flavour = yBlock.get('sys:flavour') as Flavour;
|
||||
const type = yBlock.get('prop:type') as string;
|
||||
const toMd = () =>
|
||||
deltaToMd((yBlock.get('prop:text') as YText).toDelta(), deltaConverters);
|
||||
const hidden = yBlock.get('prop:hidden') as boolean;
|
||||
const displayMode = yBlock.get('prop:displayMode') as string;
|
||||
const childrenIds =
|
||||
yBlock.get('sys:children') instanceof YArray
|
||||
? (yBlock.get('sys:children') as YArray<string>).toJSON()
|
||||
: [];
|
||||
|
||||
let result: ParsedBlock = {
|
||||
id,
|
||||
flavour,
|
||||
content: '',
|
||||
children: [],
|
||||
type,
|
||||
};
|
||||
|
||||
if (hidden || displayMode === 'edgeless') {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (flavour) {
|
||||
case 'affine:paragraph': {
|
||||
let initial = '';
|
||||
if (type === 'h1') {
|
||||
initial = '# ';
|
||||
} else if (type === 'h2') {
|
||||
initial = '## ';
|
||||
} else if (type === 'h3') {
|
||||
initial = '### ';
|
||||
} else if (type === 'h4') {
|
||||
initial = '#### ';
|
||||
} else if (type === 'h5') {
|
||||
initial = '##### ';
|
||||
} else if (type === 'h6') {
|
||||
initial = '###### ';
|
||||
} else if (type === 'quote') {
|
||||
initial = '> ';
|
||||
}
|
||||
result.content = initial + toMd() + '\n';
|
||||
break;
|
||||
}
|
||||
case 'affine:divider': {
|
||||
result.content = '\n---\n\n';
|
||||
break;
|
||||
}
|
||||
case 'affine:list': {
|
||||
result.content = (type === 'bulleted' ? '* ' : '1. ') + toMd() + '\n';
|
||||
break;
|
||||
}
|
||||
case 'affine:code': {
|
||||
const lang =
|
||||
(yBlock.get('prop:language') as string)?.toLowerCase() || 'txt';
|
||||
// do not transform to delta for code block
|
||||
const caption = yBlock.get('prop:caption') as string;
|
||||
result.content =
|
||||
'```' +
|
||||
lang +
|
||||
(caption ? ` ${caption}` : '') +
|
||||
'\n' +
|
||||
(yBlock.get('prop:text') as YText).toJSON() +
|
||||
'\n```\n\n';
|
||||
break;
|
||||
}
|
||||
case 'affine:image': {
|
||||
const sourceId = yBlock.get('prop:sourceId') as string;
|
||||
const width = yBlock.get('prop:width');
|
||||
const height = yBlock.get('prop:height');
|
||||
// fixme: this may not work if workspace is not public
|
||||
const blobUrl = context.buildBlobUrl(sourceId);
|
||||
const caption = yBlock.get('prop:caption') as string;
|
||||
if (width || height || caption) {
|
||||
result.content =
|
||||
`<img
|
||||
src="${blobUrl}"
|
||||
alt="${caption}"
|
||||
width="${width || 'auto'}"
|
||||
height="${height || 'auto'}"
|
||||
/>
|
||||
` + '\n\n';
|
||||
} else {
|
||||
result.content = `\n\n\n`;
|
||||
}
|
||||
Object.assign(result, {
|
||||
sourceId,
|
||||
width,
|
||||
height,
|
||||
caption,
|
||||
blobUrl,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case 'affine:attachment': {
|
||||
const sourceId = yBlock.get('prop:sourceId') as string;
|
||||
const blobUrl = context.buildBlobUrl(sourceId);
|
||||
const caption = yBlock.get('prop:caption') as string;
|
||||
if (type.startsWith('video')) {
|
||||
result.content =
|
||||
`<video muted autoplay loop preload="auto" playsinline>
|
||||
<source src="${blobUrl}" type="${type}" />
|
||||
</video>
|
||||
` + '\n\n';
|
||||
} else {
|
||||
// assume it is an image
|
||||
result.content = `\n\n\n`;
|
||||
}
|
||||
Object.assign(result, {
|
||||
sourceId,
|
||||
blobUrl,
|
||||
caption,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'affine:embed-youtube': {
|
||||
const videoId = yBlock.get('prop:videoId') as string;
|
||||
// prettier-ignore
|
||||
result.content = `
|
||||
<iframe
|
||||
type="text/html"
|
||||
width="100%"
|
||||
height="410px"
|
||||
src="https://www.youtube.com/embed/${videoId}"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
credentialless>
|
||||
</iframe>` + '\n\n';
|
||||
break;
|
||||
}
|
||||
case 'affine:bookmark': {
|
||||
const url = yBlock.get('prop:url') as string;
|
||||
const caption = yBlock.get('prop:caption') as string;
|
||||
result.content = `\n[](Bookmark,${url})\n\n`;
|
||||
Object.assign(result, {
|
||||
url,
|
||||
caption,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'affine:embed-linked-doc':
|
||||
case 'affine:embed-synced-doc': {
|
||||
const pageId = yBlock.get('prop:pageId') as string;
|
||||
const caption = yBlock.get('prop:caption') as string;
|
||||
result.content = `\n[${caption}](${context.buildDocUrl(pageId)})\n\n`;
|
||||
Object.assign(result, {
|
||||
pageId,
|
||||
caption,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'affine:surface':
|
||||
case 'affine:page':
|
||||
case 'affine:note':
|
||||
case 'affine:frame': {
|
||||
result.content = '';
|
||||
break;
|
||||
}
|
||||
case 'affine:database': {
|
||||
const title = (yBlock.get('prop:title') as YText).toJSON();
|
||||
const childrenTitleById = Object.fromEntries(
|
||||
childrenIds.map(cid => {
|
||||
const child = parseBlock(
|
||||
context,
|
||||
yBlocks.get(cid) as YBlock | undefined,
|
||||
yBlocks
|
||||
);
|
||||
if (!child) {
|
||||
return [cid, ''];
|
||||
}
|
||||
return [cid, parseBlockToMd(child)] as const;
|
||||
})
|
||||
);
|
||||
const cols = (
|
||||
yBlock.get('prop:columns') as YArray<ColumnDataType>
|
||||
).toJSON() as ColumnDataType[];
|
||||
|
||||
const cells = (
|
||||
yBlock.get('prop:cells') as YMap<SerializedCells>
|
||||
).toJSON() as SerializedCells;
|
||||
|
||||
const optionToTagHtml = (option: any) => {
|
||||
return `<span data-affine-option data-value="${option.id}" data-option-color="${option.color}">${option.value}</span>`;
|
||||
};
|
||||
|
||||
const dbRows: string[][] = childrenIds
|
||||
.map(cid => {
|
||||
const row = cells[cid];
|
||||
return cols.map(col => {
|
||||
const value = row?.[col.id]?.value;
|
||||
|
||||
if (col.type !== 'title' && !value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (col.type) {
|
||||
case 'title':
|
||||
return childrenTitleById[cid];
|
||||
case 'select':
|
||||
return optionToTagHtml(
|
||||
(col.data['options'] as any).find(
|
||||
(opt: any) => opt.id === value
|
||||
)
|
||||
);
|
||||
case 'multi-select':
|
||||
return (col.data['options'] as any)
|
||||
.filter((opt: any) => (value as string[]).includes(opt.id))
|
||||
.map(optionToTagHtml)
|
||||
.join('');
|
||||
default:
|
||||
return value ?? '';
|
||||
}
|
||||
});
|
||||
})
|
||||
.filter(row => !row.every(v => !v));
|
||||
const header = cols.map(col => {
|
||||
return col.name;
|
||||
});
|
||||
|
||||
const divider = cols.map(() => {
|
||||
return '---';
|
||||
});
|
||||
|
||||
// convert to markdown table
|
||||
result.content =
|
||||
[header, divider, ...dbRows]
|
||||
.map(row => {
|
||||
return (
|
||||
'|' +
|
||||
row
|
||||
.map(cell => String(cell || '')?.trim())
|
||||
.join('|')
|
||||
.replace(/\n+/g, '<br />') +
|
||||
'|'
|
||||
);
|
||||
})
|
||||
.join('\n') + '\n\n';
|
||||
|
||||
Object.assign(result, {
|
||||
title,
|
||||
rows: dbRows.map(row => {
|
||||
return Object.fromEntries(row.map((v, i) => [cols[i].name, v]));
|
||||
}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'affine:table': {
|
||||
// Extract row IDs and their order
|
||||
const rowEntries = Object.entries(yBlock.toJSON())
|
||||
.filter(
|
||||
([key]) => key.startsWith('prop:rows.') && key.endsWith('.rowId')
|
||||
)
|
||||
.map(([key, value]) => {
|
||||
const rowId = value as string;
|
||||
const orderKey = key.replace('.rowId', '.order');
|
||||
const order = yBlock.get(orderKey) as string;
|
||||
const backgroundColor = yBlock.get(
|
||||
key.replace('.rowId', '.backgroundColor')
|
||||
) as string | undefined;
|
||||
return { rowId, order, backgroundColor };
|
||||
})
|
||||
.sort((a, b) => a.order.localeCompare(b.order));
|
||||
|
||||
// Extract column IDs and their order
|
||||
const columnEntries = Object.entries(yBlock.toJSON())
|
||||
.filter(
|
||||
([key]) =>
|
||||
key.startsWith('prop:columns.') && key.endsWith('.columnId')
|
||||
)
|
||||
.map(([key, value]) => {
|
||||
const columnId = value as string;
|
||||
const orderKey = key.replace('.columnId', '.order');
|
||||
const order = yBlock.get(orderKey) as string;
|
||||
return { columnId, order };
|
||||
})
|
||||
.sort((a, b) => a.order.localeCompare(b.order));
|
||||
|
||||
// Build the table rows with cell data
|
||||
const tableRows = rowEntries.map(({ rowId }) => {
|
||||
return columnEntries.map(({ columnId }) => {
|
||||
const cellKey = `prop:cells.${rowId}:${columnId}.text`;
|
||||
const cellText = yBlock.get(cellKey) as string | undefined;
|
||||
return cellText || '';
|
||||
});
|
||||
});
|
||||
|
||||
// Store column IDs for reference
|
||||
const columnIds = columnEntries.map(({ columnId }) => columnId);
|
||||
|
||||
// Use the first row as header and the rest as data rows
|
||||
if (tableRows.length > 0) {
|
||||
const headerRow = tableRows[0];
|
||||
const dataRows = tableRows.slice(1);
|
||||
const separators = headerRow.map(() => '---');
|
||||
|
||||
// Convert to markdown table with first row as header
|
||||
result.content =
|
||||
[headerRow, separators, ...dataRows]
|
||||
.map(row => {
|
||||
return (
|
||||
'|' +
|
||||
row
|
||||
.map(cell => String(cell || '')?.trim())
|
||||
.join('|')
|
||||
.replace(/\n+/g, '<br />') +
|
||||
'|'
|
||||
);
|
||||
})
|
||||
.join('\n') + '\n\n';
|
||||
} else {
|
||||
// Handle empty table case
|
||||
result.content = '';
|
||||
}
|
||||
|
||||
Object.assign(result, {
|
||||
columns: columnIds,
|
||||
rows: tableRows,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// console.warn("Unknown or unsupported flavour", flavour);
|
||||
}
|
||||
}
|
||||
|
||||
result.children =
|
||||
flavour !== 'affine:database'
|
||||
? childrenIds
|
||||
.map(cid =>
|
||||
parseBlock(
|
||||
context,
|
||||
yBlocks.get(cid) as YBlock | undefined,
|
||||
yBlocks
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
(block): block is ParsedBlock =>
|
||||
!!block &&
|
||||
!(block.content === '' && block.children.length === 0)
|
||||
)
|
||||
: [];
|
||||
} catch (e) {
|
||||
console.warn('Error converting block to md', e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const parsePageDoc = (ctx: ParserContext): ParsedDoc => {
|
||||
// we assume that the first block is the page block
|
||||
const yBlocks: YBlocks = ctx.doc.getMap('blocks');
|
||||
const maybePageBlock = Object.entries(yBlocks.toJSON()).findLast(
|
||||
([_, b]) => b['sys:flavour'] === 'affine:page'
|
||||
);
|
||||
|
||||
// there are cases that the page is empty due to some weird issues
|
||||
if (!maybePageBlock) {
|
||||
return {
|
||||
title: '',
|
||||
md: '',
|
||||
};
|
||||
} else {
|
||||
const yPage = yBlocks.get(maybePageBlock[0]) as YBlock;
|
||||
const title = yPage.get('prop:title') as YText;
|
||||
const rootBlock = parseBlock(ctx, yPage, yBlocks);
|
||||
if (!rootBlock) {
|
||||
return {
|
||||
title: '',
|
||||
md: '',
|
||||
};
|
||||
}
|
||||
rootBlock.children = rootBlock.children.filter(
|
||||
(block): block is BaseParsedBlock => block.flavour === 'affine:note'
|
||||
);
|
||||
const md = parseBlockToMd(rootBlock);
|
||||
|
||||
return {
|
||||
title: title.toJSON(),
|
||||
parsedBlock: rootBlock,
|
||||
md,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,152 +0,0 @@
|
||||
import { type CellDataType } from '@blocksuite/affine/model';
|
||||
import { type Doc as YDoc, type Map as YMap } from 'yjs';
|
||||
|
||||
export interface WorkspacePage {
|
||||
id: string;
|
||||
guid: string;
|
||||
title: string;
|
||||
createDate: number;
|
||||
trash?: boolean;
|
||||
favorite?: boolean;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
export type BaseFlavour<T extends string> = `affine:${T}`;
|
||||
|
||||
export type Flavour = BaseFlavour<
|
||||
| 'page'
|
||||
| 'frame'
|
||||
| 'paragraph'
|
||||
| 'code'
|
||||
| 'note'
|
||||
| 'list'
|
||||
| 'divider'
|
||||
| 'embed'
|
||||
| 'image'
|
||||
| 'surface'
|
||||
| 'database'
|
||||
| 'table'
|
||||
| 'attachment'
|
||||
| 'bookmark'
|
||||
| 'embed-youtube'
|
||||
| 'embed-linked-doc'
|
||||
| 'embed-synced-doc'
|
||||
>;
|
||||
|
||||
export interface BaseParsedBlock {
|
||||
id: string;
|
||||
flavour: Flavour;
|
||||
content: string;
|
||||
children: BaseParsedBlock[];
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface ParsedDoc {
|
||||
title: string;
|
||||
md: string;
|
||||
parsedBlock?: ParsedBlock;
|
||||
}
|
||||
|
||||
export interface ParagraphBlock extends BaseParsedBlock {
|
||||
flavour: 'affine:paragraph';
|
||||
type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'quote';
|
||||
}
|
||||
|
||||
export interface DividerBlock extends BaseParsedBlock {
|
||||
flavour: 'affine:divider';
|
||||
}
|
||||
|
||||
export interface ListBlock extends BaseParsedBlock {
|
||||
flavour: 'affine:list';
|
||||
type: 'bulleted' | 'numbered';
|
||||
}
|
||||
|
||||
export interface CodeBlock extends BaseParsedBlock {
|
||||
flavour: 'affine:code';
|
||||
language: string;
|
||||
}
|
||||
|
||||
export interface ImageBlock extends BaseParsedBlock {
|
||||
flavour: 'affine:image';
|
||||
sourceId: string;
|
||||
blobUrl: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
caption?: string;
|
||||
}
|
||||
|
||||
export interface AttachmentBlock extends BaseParsedBlock {
|
||||
flavour: 'affine:attachment';
|
||||
type: string;
|
||||
sourceId: string;
|
||||
}
|
||||
|
||||
export interface EmbedYoutubeBlock extends BaseParsedBlock {
|
||||
flavour: 'affine:embed-youtube';
|
||||
videoId: string;
|
||||
}
|
||||
|
||||
export interface BookmarkBlock extends BaseParsedBlock {
|
||||
flavour: 'affine:bookmark';
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface EmbedLinkedDocBlock extends BaseParsedBlock {
|
||||
flavour: 'affine:embed-linked-doc';
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
export interface EmbedSyncedDocBlock extends BaseParsedBlock {
|
||||
flavour: 'affine:embed-synced-doc';
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
export interface DatabaseBlock extends BaseParsedBlock {
|
||||
title: string;
|
||||
flavour: 'affine:database';
|
||||
rows: Record<string, string>[];
|
||||
}
|
||||
|
||||
export interface TableBlock extends BaseParsedBlock {
|
||||
flavour: 'affine:table';
|
||||
rows: string[][];
|
||||
columns: string[];
|
||||
}
|
||||
|
||||
export type ParsedBlock =
|
||||
| ParagraphBlock
|
||||
| DividerBlock
|
||||
| ListBlock
|
||||
| CodeBlock
|
||||
| ImageBlock
|
||||
| AttachmentBlock
|
||||
| EmbedYoutubeBlock
|
||||
| BookmarkBlock
|
||||
| DatabaseBlock
|
||||
| TableBlock
|
||||
| BaseParsedBlock;
|
||||
|
||||
export interface ParsedDoc {
|
||||
title: string;
|
||||
md: string;
|
||||
parsedBlock?: ParsedBlock;
|
||||
}
|
||||
|
||||
export type SerializedCells = {
|
||||
// row
|
||||
[key: string]: {
|
||||
// column
|
||||
[key: string]: CellDataType;
|
||||
};
|
||||
};
|
||||
|
||||
export type YBlock = YMap<unknown>;
|
||||
export type YBlocks = YMap<YBlock>;
|
||||
|
||||
export interface ParserContext {
|
||||
workspaceId: string;
|
||||
doc: YDoc;
|
||||
buildBlobUrl: (blobId: string) => string;
|
||||
buildDocUrl: (docId: string) => string;
|
||||
renderDocTitle?: (docId: string) => string;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user