Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions .github/workflows/note-bridge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: note-bridge

on:
pull_request:
paths: ['note-bridge/**', '.github/workflows/note-bridge.yml']
push:
branches: [master]
tags: ['note-bridge-v*']
paths: ['note-bridge/**', '.github/workflows/note-bridge.yml']
workflow_dispatch:
inputs:
draft_release:
description: 'Create a draft release (select a note-bridge-v* tag as the run ref)'
type: boolean
default: false

permissions:
contents: read

concurrency:
group: note-bridge-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
runs-on: ubuntu-latest
defaults:
run:
working-directory: note-bridge
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 26.12.1
cache: npm
cache-dependency-path: |
note-bridge/package-lock.json
note-bridge/agent/package-lock.json
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- run: npm ci
- run: npm run agent:install
- run: npm run format:check
- run: npm run agent:build
- run: npm run agent:test
- run: npm run build
- run: npx playwright install --with-deps chromium
- run: npm run test:ui
- name: Upload desktop UI report and screenshots
if: always()
uses: actions/upload-artifact@v7
with:
name: note-bridge-ui-report
path: |
note-bridge/playwright-report/
note-bridge/test-results/
retention-days: 7
- run: cargo fmt --all --check
- run: cargo test --locked -p bridge-core
- run: cargo clippy --locked -p bridge-core --all-targets -- -D warnings

desktop:
needs: verify
strategy:
fail-fast: false
matrix:
include:
- os: macos-14
target: aarch64-apple-darwin
bundles: dmg
- os: macos-14
target: x86_64-apple-darwin
bundles: dmg
- os: windows-latest
target: x86_64-pc-windows-msvc
bundles: nsis
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: note-bridge
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 26.12.1
cache: npm
cache-dependency-path: |
note-bridge/package-lock.json
note-bridge/agent/package-lock.json
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- run: npm ci
- run: npm run agent:install
- run: npm run agent:build
- run: npm run agent:test
- name: Test core on the host platform
run: cargo test --locked -p bridge-core
- name: Build unsigned evaluation package
run: npm run desktop:build -- --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} --ci
- name: Upload package for manual platform verification
uses: actions/upload-artifact@v7
with:
name: note-bridge-${{ matrix.target }}
path: |
note-bridge/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
note-bridge/target/${{ matrix.target }}/release/bundle/nsis/*.exe
if-no-files-found: error
retention-days: 14

draft-release:
if: github.event_name == 'workflow_dispatch' && inputs.draft_release && startsWith(github.ref, 'refs/tags/note-bridge-v')
needs: desktop
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
pattern: note-bridge-*
path: packages
merge-multiple: true
- name: Verify independent release version
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
node --input-type=module -e "import fs from 'node:fs'; const v = JSON.parse(fs.readFileSync('note-bridge/package.json')).version; if (process.env.RELEASE_TAG !== 'note-bridge-v' + v) process.exit(1);"
- name: Create draft with unsigned packages
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
cat > release-notes.md <<'NOTES'
独立 note-bridge 桌面应用预览包。

此处产物尚未配置开发者签名和 macOS 公证,只用于手动评估;不能视为已经完成 Windows / macOS / 浏览器兼容性验收的正式发行版。

发布前请完成 note-bridge/README.md 中的手动验收,并接入平台签名、公证及版本校验流程。
NOTES
mapfile -d '' assets < <(find packages -type f \( -name '*.dmg' -o -name '*.exe' \) -print0)
test "${#assets[@]}" -gt 0
gh release create "$RELEASE_TAG" "${assets[@]}" --verify-tag --draft --title "$RELEASE_TAG" --notes-file release-notes.md
21 changes: 19 additions & 2 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,35 @@

## 5. 卡片与组件模式

- **知识助手**:采用居中聊天弹窗,桌面左侧会话列表、右侧安全 Markdown 消息与工具概览,底部选择已配置模型与 effort;手机使用视口内近全屏弹窗。网站是 note-bridge 的薄客户端:通过 v2 会话 RPC 和已提交状态订阅展示历史、发送文本与文档引用、显式停止回答,不在浏览器保存聊天内容、执行工具或拼装模型上下文。关闭弹窗、切换会话和路由、刷新与断开连接只结束订阅,bridge 继续运行;重连获取持久快照且不自动重发提问。聊天、工具调用结果、来源证据、压缩摘要和用量由 bridge 的 Pi durable + SQLite 保存,不导入旧浏览器聊天。正文、目录、TOC、知识网络和聚类仍由知识站点独立发布,新增 `/ai/manifest.json` 与按内容 hash 寻址的静态资源供 bridge 按需读取,内容更新不要求升级桌面安装包。附件从 bridge 获取对应版本目录并校验 contentHash;工具概览可展开查看查询/读取范围、结果及降级说明。模型窗口、输出预算和运行限制由桌面服务执行,网站只展示配置与状态。保留现有配对凭证、刷新自动连接与 SiteNav 绿色已连接标识。亮暗主题、移动端布局、焦点约束与键盘快捷键沿用现有语义令牌。

### 知识助手代码边界

知识助手沿用核心站点的领域、端口、适配器和平台分层。类型、执行逻辑和组件展示按变化原因归属,保持客户端轻量:

| 层次 | 代码位置与职责 |
| --- | --- |
| 领域契约 | `core/domain/ai/` 描述模型、连接与会话投影,`core/domain/knowledge/` 描述站点发布资源;不依赖 Vue、Node 或桥接实现 |
| 端口与核心提取 | `AiBridgePort` 定义客户端操作,`KnowledgeDocumentReaderPort` 隔离文档读取;`AiContextService` 与 `DocumentSectionExtractor` 通过注入的端口提取章节 |
| 浏览器适配 | `BrowserAiBridge` 保留业务门面,`adapters/browser/aiBridge/` 分离认证连接、凭证、Socket/心跳、重连、RPC 和入站校验;`aiMarkdown/` 独立维护链接策略、引用扩展、语法生成和最终净化 |
| 站点发布 | `platform/node/build/knowledge/` 负责来源、目录/关系投影与内容寻址;`plugins/knowledge/` 负责开发缓存及生产输出,manifest 在不可变资源之后发布 |
| 界面与生命周期 | `DocumentQA` 组装 `assistant/` 下的侧栏、消息、输入、模型和附件组件;会话分页、revision 合并、提交及附件各自维护异步状态,应用入口和配对表单使用独立 composable |

接口注释说明所有权、失败语义、幂等键、资源清理和事务边界。展开分支、循环及模板属性,保持站点已有单引号、无分号和无扩展名的 TypeScript 导入风格;已有混合风格文件只整理本次涉及片段。禁止将聊天事实另存到浏览器,或把同一份订阅状态复制到多个组件。异步回调写入界面前校验当前选择/连接版本;分页补读不能覆盖更新的推送。组件拆分要保持原 DOM、样式作用域、焦点和滚动行为,并用组件测试与桌面/移动端 E2E 验证。

### 全站导航(无顶部栏)

- `SiteNavigation` 默认收起在正文右下角,鼠标悬停时向左展开胶囊式横向导航,首页、标签、聚类和搜索入口只在横向栏提供;更多工具承载站点品牌、主题、全屏、专注模式及外链。搜索弹窗支持切换全文/目录搜索。鼠标离开后延迟收起,焦点在栏内或工具菜单打开时保持展开。
- 文档页通过 `document-tools` 插槽把 `DocTools` 并入同一个更多工具面板,保留思维导图、知识网络、知识回顾、知识助手、复制、编辑器、随机复习及字号设置;选择动作后收起导航。非文档页不挂载文档工具及其快捷键。触屏隐藏快捷键提示,编辑器入口仅 full 档可用;长菜单在导航面板内滚动,字号设置使用独立抽屉。
- 横向导航为 Note Bridge 提供独立入口,打开专用连接对话框,管理本地端口、首次配对、重新连接、断开和忘记连接。首次输入桌面端生成的一次性配对码,校验正确即连接。配对成功后持久保存当前浏览器的桥接授权凭证(不保存服务商密钥),刷新自动重连,跨页面共享连接。连接成功使用 `--success-color` 绿色与“已连接”文字共同反馈;导航收起时按钮内部仍显示状态,不占用右上角的预览恢复计数位置。断开仅暂停本页连接;忘记连接清除持久凭证,在线时同时撤销桌面端授权。窄屏横向栏允许换行,保留每个入口的完整触控尺寸。
- 文档页通过 `document-tools` 插槽把 `DocTools` 并入同一个更多工具面板,保留思维导图、知识网络、知识回顾、复制、编辑器、随机复习及字号设置;选择动作后收起导航。非文档页不挂载文档工具及其快捷键。知识助手为全站独立工具,由应用层按需挂载单个弹窗,在所有页面的更多工具中可打开,桌面支持 Alt+I;切换路由不卸载聊天。触屏隐藏快捷键提示,编辑器入口仅 full 档可用;长菜单在导航面板内滚动,字号设置使用独立抽屉。
- 移动端嵌入导航按视口限制容器宽度并从正文左缘定位,确保长表格等内容撑宽正文时,导航及文档工具入口仍留在可见范围内。
- 文档页使用正文列内的零高度 sticky 容器,宽度沿用 `--content-max`,自动跟随侧栏、章节目录及大屏居中布局;其他页面使用视口右下角。中等桌面宽度向内避让回顶控件,手机导航位于阅读底栏上方,文档工具上移一层;安全区与横屏菜单滚动共用底部偏移令牌。
- 手机阅读底栏仅保留目录、章节和回顶三个等宽入口;搜索统一从 `SiteNavigation` 打开,避免重复入口。
- 手机点击按钮展开/收起;键盘 Enter 展开并聚焦第一个链接,Escape 关闭并返回按钮,点击空白处或切换页面也会收起。导航使用真实路由链接、`aria-current` 和可见焦点,主题沿用语义令牌,所有按钮触控尺寸不少于 `--site-control-size`。
- 页面顶部不预留 Header 空间,文档侧栏、标签页和章节目录从 `top: 0` 开始粘附;底部不再为全站导航预留空白。旧 `showHeader` 偏好不再影响布局。
- 专注模式收起文档侧栏,并在浏览器支持时进入全屏;右侧导航按钮始终可达,侧栏通过原有开关恢复。
- 横向导航提供“返回最近文档”的阅读图标,与首页、标签、聚类平级,悬停提示上次成功阅读的文档标题;无阅读记录或已在该文档时禁用并提示原因。每次展开导航时刷新目标,复用原有持久化阅读记录和阅读位置恢复;文档加载成功即更新记录,加载中或失败页的滚动不写入阅读记录。手机默认在按钮上方展开入口栏,保证 320px 窄屏仍保留完整触控尺寸;自定义拖动位置继续按可用空间选择展开方向。
- 导航按钮支持鼠标、触屏拖拽,移动超过 6px 后收起导航并调整位置,松开后将相对视口的位置持久化到本地;未拖动时保留正文右下角默认定位。自定义位置跨页面及刷新保持,缩放或旋转时按可用区域恢复并约束边界。横向空间不足时入口栏在按钮上方或下方展开,按钮位置保持不变;取消拖拽不保存,更多工具可“恢复默认位置”。
- 导航按钮支持鼠标、触屏拖拽,移动超过 6px 后收起导航并调整位置,松开后将相对视口的位置持久化到本地;未拖动时保留正文右下角默认定位。自定义位置跨页面及刷新保持,缩放或旋转时按可用区域恢复并约束边界。横向空间不足时入口栏在按钮上方或下方展开,按钮位置保持不变;锚点始终保持单按钮尺寸,入口栏换行不改变拖拽定位。取消拖拽不保存,更多工具可“恢复默认位置”。

### 卡片(基线)
```less
Expand Down
10 changes: 10 additions & 0 deletions note-bridge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules/
dist/
target/
test-results/
playwright-report/
src-tauri/icons/
src-tauri/gen/
*.log
.DS_Store
src-tauri/binaries/
13 changes: 13 additions & 0 deletions note-bridge/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"singleQuote": true,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte",
"htmlWhitespaceSensitivity": "ignore"
}
}
]
}
51 changes: 51 additions & 0 deletions note-bridge/AGENT_V2_CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Agent v2 implementation contract

Implementation target agreed 2026-10-24. Fresh AI storage only; no import of old browser histories. Retain existing pairing grants/configuration. New chat protocol v2 only, with an explicit upgrade error for v1.

## Website WebSocket

All frames have v:2. Keep pair/auth/auth.forget/ping and auth.ok/providers.updated/pong (provider metadata unchanged). auth.ok adds capabilities:["persistent_sessions","knowledge_tools"].

Client RPC: `{v:2,type:"request",id,method,params}`.
Response: `{v:2,type:"response",id,result}` or `{v:2,type:"response",id,error:{code,message}}`.
Push: `{v:2,type:"session.updated",sessionId,revision,session,turn?}`. session is a summary; turn is the complete bounded current/changed turn, replaced by id. Updates are committed projections, throttled; they are not raw Pi events. Reconnect resubscribes and reloads latest page; no message resubmission. Also `{v:2,type:"sessions.changed"}` for list refresh.

RPC methods:
- sessions.list {cursor?,limit?} => {sessions: SessionSummary[],nextCursor?}; newest first; default/max 20/50.
- sessions.create {providerId,model,effort?} => SessionSummary. Knowledge origin comes from authenticated socket, never browser params.
- sessions.get {sessionId,cursor?,limit?} => {session:SessionSummary,turns:ChatTurn[],nextCursor?}; latest page chronological, cursor loads earlier page; default/max 20/50.
- sessions.archive {sessionId} => {ok:true}; reject if active, archival persistent.
- session.subscribe {sessionId} => {ok:true}; followed by committed updates; UI subscribe before get to avoid race and reconcile revisions.
- session.unsubscribe {sessionId} => {ok:true}.
- messages.send {sessionId,clientRequestId,text,providerId,model,effort?,attachments?:[{docId,headingIds?:string[],contentHash?:string}]} => {runId,turnId}; defaults max question 20k chars, attachments 3. Same id+same normalized payload returns same ids; conflict rejects. Busy rejects. Runtime binds current site manifest revision per run.
- runs.cancel {sessionId} => {ok:true}; terminal cancellation is durable, only explicit cancel stops a connected/disconnected run.
- knowledge.document {docId} => {docId,title,url,contentHash,headings:[{id,title}]}; optional attachment picker reads bridge-approved current site metadata.

SessionSummary: {id,title,providerId,model,effort,createdAt,updatedAt,status:"idle"|"running"|"error",revision:number}.
ChatTurn: {id,question,answer,status:"running"|"complete"|"cancelled"|"error",sources:Citation[],toolCalls:ToolCallRecord[],error?:string,contextNotice?:string,createdAt:number}.
Citation: {sourceId,title,url,contentHash?,revision?}; url is site-relative canonical .html link with optional anchor.
ToolCallRecord: {callId,name,status:"running"|"complete"|"error",message?,overview?:{input:[{label,value}],summary?,items?:string[],note?}}.
Keep existing markdown and tool overview rendering styles. Website does not execute tools, compose model history, save chat content, or cancel on close/navigation/disconnect. Only grant token/port/UI preferences remain in localStorage.

## Private Rust / Node IPC

JSONL UTF-8 on stdin/stdout, no stdout logs. Replies `{id,result}` or `{id,error:{code,message}}`. Limit individual IPC lines 16 MiB; bounded queues and timeouts. Async push `{event:"session.updated"|"sessions.changed",connectionId,data}` maps to the WebSocket push above. stderr must be sanitized.

Rust requests:
- initialize {dataDir,providers:RuntimeProvider[],authorizations:[{id,origin}]} => {ready:true,version:2}. Rust supplies only enabled providers, reads credentials from Keychain and transmits via this private pipe, never command line. RuntimeProvider uses existing Rust Provider fields (id,name,protocol,baseUrl,model,models,authKind,enabled) plus optional secret. No credentials go into SQLite/checkpoints.
- configure {providers,authorizations} => {ok:true}; updates runtime configuration, revocation aborts affected runs. All requests/compactions/tool boundaries recheck authorization/config and hard budget.
- request {connectionId,authorizationId,origin,request:{id,method,params}} => RPC result; exact auth grant must match origin. Rust forwards only allowlisted RPC methods.
- detach {connectionId} => {ok:true}; drops subscriptions only, never aborts generation.
- shutdown {} => {ok:true}; closes Pi storage preserving interrupted tasks; explicit cancellation remains cancelled.

Node entry point builds to agent/dist/index.js; package owns independent pinned package-lock. Rust launches a packaged Node runtime + entry, or explicit developer path. Package Pi durable/ai/chord 1.4.0 after checking installed API (main differs). Worker owns SQLite, no other process writes it. Runtime exports testable service and accepts injected provider/mock fetch where useful.

## Site knowledge contract

Knowledge data stays site-hosted, independently published. Runtime GETs authenticated origin + /ai/manifest.json; only fixed root path, no browser/model supplied origin. Node knowledge adapter pins manifest revision for a run and validates data schemas and hashes. All resource paths same origin and under /ai/, redirects rejected. No full-corpus download requirement.

Site/knowledge agent owns exact manifest/doc schema and sends it to runtime implementer. It must publish catalog, graph, clusters and structured doc sections with original site anchors, resource hashes and version, plus optional public search descriptor. Use immutable hash paths within current site build; if old assets disappear during an update return explicit stale revision instead of mixing versions. Browser SW must not cache manifest with generic stale-while-revalidate policy.

Knowledge adapter exposes a small plain TypeScript class/service and six tools usable by runtime without browser/core source imports. Use only local package dependencies or node builtins. Knowledge tool result `{data,sources?,overview?}`; bounded ~192KiB UTF-8 incl metadata, paginated text/TOC/list. Runtime maps data into Pi output and overview/sources into durable details.

Knowledge adapter interface to coordinate: `KnowledgeClient(origin, options?)`, `pin(signal?):Promise<KnowledgeSnapshot>`, snapshot `execute(name,args,signal):Promise<{data,sources?,overview?}>`, `document(docId,signal?):Promise<{docId,title,url,contentHash,headings}>`; export `KNOWLEDGE_TOOL_DEFINITIONS` with name,description,inputSchema. Tools names unchanged. Registry schema conversion is runtime-owned.
Loading
Loading