Retire Convex paths and standardize local-first Pi runtime
This commit is contained in:
@@ -1,136 +1,50 @@
|
|||||||
# 仓库协作指南(AGENTS)
|
# 仓库协作指南(AGENTS)
|
||||||
|
|
||||||
## 当前主线
|
## 主线(一句话)
|
||||||
|
|
||||||
- 产品:`tree-first graph kernel` + local-first 工作区;云首发同栈(单机/云服务器均可部署)。
|
tree-first kernel + local-first 工作区;真相 = 本地 folder + `.md`;**mnote-web:3000**;Page AI = **Pi Lab only**;控制面默认 **libsql-local**;知识库 LightRAG;Vault + `$mnote-vault`。
|
||||||
- 形态:`VSCode 简化版工作区 + leptos-tiptap Markdown + Pi Lab Page AI + LightRAG + mindmap/OnlyOffice + Wolai 主题壳 + libSQL 控制面 + Vault`。
|
SSOT:`ARCHITECTURE.md`。预发:`design/10-review/process/22-pre-release-legacy-purge-v1.md`。
|
||||||
- 数据真相:本地(或服务器上的)workspace folder;页面正文为 `.md`。
|
|
||||||
- 控制面:`MNOTE_CONTROL_PLANE_BACKEND=libsql-local` 为默认与云首发推荐(独立部署、无需 Turso 云账号);`turso-remote` / `turso-local-replica` 仅在明确需要托管同步时使用。`sqlite` 不是 mnote-web 运行时。
|
|
||||||
- Web:`mnote-web` 为 **3000** 唯一公开入口。kernel 持有树/边/projection/command;前端只消费稳定 projection。
|
|
||||||
- Page AI:**Pi Lab**(`/api/page-ai/pi/*`)为唯一产品入口。
|
|
||||||
- 知识库:LightRAG + `mnote.knowledge_rag.*` / `/api/knowledge-rag/*`。
|
|
||||||
- 正文保存:`page.body.write` + 文件版本;AI 写文件:授权 scope + allowed roots + Pi patch/diff + watcher。
|
|
||||||
- 架构 SSOT:`ARCHITECTURE.md`。预发瘦身清单:`design/10-review/process/22-pre-release-legacy-purge-v1.md`。产品化收口:`design/10-review/process/21-mvp-post-architecture-closure-checklist-v1.md`。
|
|
||||||
|
|
||||||
## 云首发范围(必须保留)
|
## 云首发必须保留
|
||||||
|
|
||||||
| 能力 | 入口 / 说明 |
|
工作区树文档 · Pi Lab · LightRAG · OnlyOffice · Vault · libsql-local。
|
||||||
|------|-------------|
|
勿当主链宣传:第三方 ACP 宿主、默认 debug tree shell。
|
||||||
| 工作区 + 树 + 文档 | kernel + FileTree/PageTree + leptos-tiptap |
|
|
||||||
| Page AI | Pi Lab |
|
|
||||||
| 知识库 | LightRAG |
|
|
||||||
| 办公文档 | OnlyOffice(附件/资源进入) |
|
|
||||||
| 密码箱 | Vault + `$mnote-vault` |
|
|
||||||
| 控制面 | `libsql-local`(auth / membership / share / AI policy / Pi scope) |
|
|
||||||
|
|
||||||
不在首发产品面、且不得在文档中当作可选主链宣传的路径:旧 agent 网关、第三方 ACP 宿主、OpenCode 代理壳、debug tree shell(默认关闭)。清理顺序与可删文件见 purge 清单。
|
## 约束
|
||||||
|
|
||||||
## 组件定位
|
- 树/边/结构只在 Rust kernel;UI 不拼第二套真相。
|
||||||
|
- 浏览器禁止新增 `setInterval` 轮询 → WS/SSE/watcher/command/MO。
|
||||||
|
- 标题/设置/正文 → Page Aggregate;树变更 → `tree.*`。
|
||||||
|
- AI 写文件:scope + roots + Pi patch/diff + 版本 + watcher(Phase C 流式 apply 冻结)。
|
||||||
|
- `mnote_agent_tools/` 与 `/api/mnote/tools/*` **不可整目录误删**。
|
||||||
|
|
||||||
| 组件 | 定位 |
|
## 目录
|
||||||
|------|------|
|
|
||||||
| `leptos-tiptap` island | 文档页默认编辑 runtime;非对象真源 |
|
|
||||||
| Mindmap | tree-first 视图/挂件;非真相层 |
|
|
||||||
| OnlyOffice | 独立页面编辑器;经附件/资源进入 |
|
|
||||||
| LightRAG + Pi | 知识库与 Page AI 默认融合 |
|
|
||||||
| Vault | 本机/服务器密码箱;AI 经 capability token 读密 |
|
|
||||||
| Resource → File → Page Tree | 对象真源 → 主组织投影 → 导航投影 |
|
|
||||||
|
|
||||||
## Runtime 模块定位
|
|
||||||
|
|
||||||
- Browser:`rust/crates/mnote-web/browser/`
|
|
||||||
- Sidebar:`sidebar-tree-runtime.js`(及 workspace / page tree / filetree / upload 等)
|
|
||||||
- FileTree:`filetree-*.js`
|
|
||||||
- Page AI host:`sidebar-page-ai-*.js`(默认 Pi Lab)
|
|
||||||
- 文档 host:`document-editor-adapter-runtime.js`
|
|
||||||
- Island:`rust/spikes/leptos-tiptap-spike/src/editor_runtime/`
|
|
||||||
- SSR 注入:`layout.rs` / `tree.rs` / `web_shell.rs` — 非大型 JS 功能定位入口
|
|
||||||
|
|
||||||
## 目录优先级
|
|
||||||
|
|
||||||
| 路径 | 用途 |
|
| 路径 | 用途 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `rust/crates/core-protocol/` | Kernel 类型、projection 协议 |
|
| `rust/crates/core-protocol` · `bridge-runtime` · `mnote-web` | 协议 / query-command / 3000 |
|
||||||
| `rust/crates/bridge-runtime/` | query / command / traversal |
|
| `mnote-web/browser` · `leptos-tiptap-spike` | 前端 runtime / 编辑 island |
|
||||||
| `rust/crates/mnote-web/` | 3000 route、transport、projection |
|
| `mnote-vault-core` · `src/components/onlyoffice` | Vault · OO |
|
||||||
| `rust/crates/mnote-web/browser/` | 浏览器 runtime JS |
|
| `recycle/` · `reference-code/` | 非实现依据;**不进部署包** |
|
||||||
| `rust/spikes/leptos-tiptap-spike/` | 文档页编辑 island |
|
|
||||||
| `rust/crates/mnote-vault-core/` | Vault 核心 |
|
|
||||||
| `src/components/onlyoffice/` | OnlyOffice 静态与插件 |
|
|
||||||
| `wolai-backend/app/` | 辅助后端 / 异步(非 3000 主入口) |
|
|
||||||
| `recycle/` | 回收区;不作为实现依据 |
|
|
||||||
| `reference-code/` | 对照参考;**不进部署包** |
|
|
||||||
|
|
||||||
## 架构约束
|
## 协作
|
||||||
|
|
||||||
- 树 / 页面结构 / 引用边:落 Rust kernel,禁止 UI 拼第二套真相。
|
- 只改任务相关文件;不覆盖用户改动。
|
||||||
- 浏览器主链禁止新增 `setInterval` / 周期轮询;用 WS/SSE、watcher、command result、`MutationObserver`。
|
- design:`process/` 可执行,`done/` 完成。bugs 镜像大类。
|
||||||
- 标题 / 页面设置 / 正文:收口 Page Aggregate。
|
- CodeGraph 结构问题;`rg` 字面量。热启:`npm run dev:hot`。测:`cargo test -p mnote-web`。
|
||||||
- 新建/重命名/移动/归档:`tree.*`。
|
|
||||||
- AI 正文:文件引用 + `AiAccessScope` + Pi patch/diff + 版本冲突 + watcher。Phase C 流式 apply 冻结。
|
|
||||||
- Page AI 宿主为原生 Pi Lab runtime;普通 UI 不得出现第二套 agent 切换入口。
|
|
||||||
- Agent 工具包:`rust/crates/mnote-web/src/mnote_agent_tools/`(HTTP 面 `/api/mnote/tools/*`;Pi / LightRAG / skill / OnlyOffice live tools 依赖,**不可整目录误删**)。
|
|
||||||
- 架构判断:`ARCHITECTURE.md` → 对应 `design/**` process/done 稿。
|
|
||||||
|
|
||||||
## 设计稿目录规则
|
## 账号(dev,勿混)
|
||||||
|
|
||||||
- `design/01-*` … `design/12-*`:`process/` 可执行,`done/` 已完成,`reference/` 参考,`draft/` 未成形。
|
| 账号 | 角色 |
|
||||||
- 已完成主线稿迁 `done/`;被覆盖旧稿迁 `design/old/` 并标 `[recycle]`。
|
|------|------|
|
||||||
- `design/90-reference/` 只放参考资料。
|
| `mnote-admin` | ops admin |
|
||||||
|
| `mnote-e2e` | `ai_service`(非 admin) |
|
||||||
|
| 其它 | human |
|
||||||
|
|
||||||
## Bugs 目录规则
|
详情与密码:`scripts/TESTING_REFERENCE.md`。登录走标准 `/auth`(无快速登录)。
|
||||||
|
|
||||||
- 镜像 design 大类;`process/` 未修完,`done/` 已验证。
|
## 指针
|
||||||
- 壳/Sidebar/文档页体验 → `05-editor-mainline/`;projection/selection/DnD/tree → `04-tree-domain/`。
|
|
||||||
|
|
||||||
## 协作边界
|
- Vault:`vault-policy.md` · `$mnote-vault`(pack id `mnote-vault`)。
|
||||||
|
- **知识中枢**(非 LightRAG):`/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space`
|
||||||
- 只改与任务直接相关的文件;不覆盖用户已有改动;无关脏改动保持不动。
|
- 查 → `INDEX.md`;更 → 事实变更时;写/多域 → `$mnote-e2e-knowledge-hub`
|
||||||
- Subagent 为受控叶子:限定读写范围与验证命令;主控复核 diff/证据后才采纳。
|
|
||||||
- 同一写入范围同时只允许一个 writer。
|
|
||||||
|
|
||||||
## CodeGraph
|
|
||||||
|
|
||||||
- 结构性代码问题优先 CodeGraph MCP(explore / impact)。
|
|
||||||
- 改代码后 `codegraph sync .`;大重构或索引异常 `codegraph index . --force`。
|
|
||||||
- 字面文本/日志字符串优先 `rg`。
|
|
||||||
|
|
||||||
## Reference-Code
|
|
||||||
|
|
||||||
- 优先 `reference-code/sidex-main`;`reference-code/vscode` 为裁剪辅助。
|
|
||||||
- 每次只对照一个切面;不要把参考项目 UI 状态当成 MNote 新事实源。
|
|
||||||
|
|
||||||
## 常用命令
|
|
||||||
|
|
||||||
- 热启动:`npm run dev:hot` → `http://localhost:3000`
|
|
||||||
- 测试:`cargo test -p mnote-web`
|
|
||||||
- Control-plane 默认:`MNOTE_CONTROL_PLANE_BACKEND=libsql-local`
|
|
||||||
- 云首发:同 `libsql-local` + 服务器本地数据目录;需要托管再换 `turso-remote`
|
|
||||||
|
|
||||||
## Smoke 与测试账号
|
|
||||||
|
|
||||||
- 基线:`scripts/TESTING_REFERENCE.md`;默认入口 `3000 + leptos-tiptap + local-first + libSQL auth`。
|
|
||||||
- 禁止 smoke 用 `sqlite3` CLI 直写 control-plane;seed 走 Rust API / `scripts/lib/control-plane-dev-seed.js`。
|
|
||||||
- 账号角色(7-76 方案 A,**勿混用**):
|
|
||||||
| 账号 | 定位 | 密码(本地/dev) | admin 能力 |
|
|
||||||
|------|------|------------------|------------|
|
|
||||||
| `mnote-admin` / `mnote.admin@example.com` | **ops admin**(`/admin/*`、代签、policy) | `MnoteAdmin123!` | 是 |
|
|
||||||
| `mnote-e2e` / `mnote.e2e@example.com` | **AI 主体** `ai_service`(私有工作区/知识库) | `MnoteE2E123!` | **否** |
|
|
||||||
| `liaibo` 等 | 普通 human user | 各自密码 | **否** |
|
|
||||||
- 登录走 **标准** `/auth` 表单或 `/api/auth`(已移除「测试账号快速登录」);开发对标生产。
|
|
||||||
- Admin 列表真源:`access-policy.json` 的 `admins` + `MNOTE_ADMIN_USER_IDS`(默认仅 `mnote-admin`)。
|
|
||||||
|
|
||||||
## 前端测试
|
|
||||||
|
|
||||||
- 真实渲染:`/doko`;交互回归:浏览器自动化。
|
|
||||||
- 主页/Sidebar/文档首屏优先复用 `scripts/task*-smoke.js`。
|
|
||||||
|
|
||||||
## 编码
|
|
||||||
|
|
||||||
- UTF-8;TS/TSX 2 空格;Python 4 空格。
|
|
||||||
|
|
||||||
## 密码箱 / agent skills(薄指针)
|
|
||||||
|
|
||||||
- 策略 SSOT:`/home/lix/.agent-infra/vault-policy.md`;skill `$mnote-vault`(`skills/mnote-vault`)。
|
|
||||||
- Page AI pack 注册:agent tool pack 内 `skill` 模块 id `mnote-vault`(仅放 `skills/` 不够)。
|
|
||||||
- 勿在本文件复制完整 vault 策略。
|
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
# 5-38 双向链接系统(Bidirectional Links)v1
|
||||||
|
|
||||||
|
> 状态:process(调研完成,待分阶段落地)
|
||||||
|
> 日期:2026-07-29
|
||||||
|
> 目标:避免「多处引用同一知识却各写一份」的多真源问题;在 local-first + tree-first kernel 下建立**单真源引用边 + 反链投影**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 一句话结论
|
||||||
|
|
||||||
|
**正向链接写在正文(Markdown / block 语义)→ 保存/索引时抽出 `references` 边 → 反链只是该边的反向投影。**
|
||||||
|
TipTap 只负责插入/渲染;**不**在前端维护第二套链接图。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. TipTap 更新检查(2026-07-29)
|
||||||
|
|
||||||
|
| 位置 | 版本 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `reference-code/leptos-tiptap/tiptap` | **@tiptap/\* 2.27.2** | 当前主链 bridge 依赖 |
|
||||||
|
| npm `v2-latest` | **2.27.2** | v2 线已停在此点 |
|
||||||
|
| npm `latest` | **3.29.2**(2026-07-28) | 主线已进 v3 很久 |
|
||||||
|
| `reference-code/tiptap-main` 快照 | packages 约 **3.23.4**,git ~2026-05 | 参考仓库偏旧于 npm latest |
|
||||||
|
| `@tiptap/extension-mention` | 3.29.2 与 core 对齐 | v3 线 mention;v2 仍有独立包 |
|
||||||
|
|
||||||
|
### 1.1 对双向链接的含义
|
||||||
|
|
||||||
|
- **双向链接不依赖 TipTap 3**。核心是 kernel 边 + Markdown 持久化 + 索引投影。
|
||||||
|
- **本阶段不建议为双向链接单独升 v3**。v2→v3 是独立迁移(schema/API/extension 打包方式变化),成本远高于「补 page mention + 反链面板」。
|
||||||
|
- 可继续参考:
|
||||||
|
- `reference-code/leptos-tiptap`(当前 bridge / link extension)
|
||||||
|
- `reference-code/tiptap-notion-like-registry` 的 `mention-dropdown-menu` / `link-popover`(**UI 交互**,不是用户 mention 语义)
|
||||||
|
- `reference-code/tiptap-main` 仅作 API 对照;升 v3 时再同步拉取
|
||||||
|
|
||||||
|
### 1.2 可选后续(非本系统阻塞)
|
||||||
|
|
||||||
|
- 独立 checklist:评估 leptos-tiptap bridge 升 `@tiptap/*@3` 的 breaking 面。
|
||||||
|
- 升版前冻结:现有 `page_reference` ↔ link mark round-trip 测试。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 现状盘点(已有资产 / 缺口)
|
||||||
|
|
||||||
|
### 2.1 已有(可复用,勿重造)
|
||||||
|
|
||||||
|
| 层 | 资产 | 路径/入口 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 语法 | `[[page\|label]]` / `((block\|label))` | `mnote-editor-core/src/markdown.rs` |
|
||||||
|
| 块类型 | `page_reference` / `PageReference` | core-protocol、bridge、editor actor |
|
||||||
|
| 编辑投影 | page_ref → 段落 + `link` mark(class `mnote-page-block-link`) | `document-tiptap-conversion-runtime.js`、`block_transform.rs` |
|
||||||
|
| 索引反链 | 扫 `](*.md)` + `[[…]]`,API 可读 | `local_search_index.rs` → `GET /api/search/local-index/backlinks` |
|
||||||
|
| Kernel 类型 | `KernelEdgeType::References` / `BacklinksTo` | `core-protocol/src/kernel.rs` |
|
||||||
|
| 页面选项 | `collapseBacklinks` / `showBlockRefCount` | page aggregate + UI preferences |
|
||||||
|
| 证据边 | markdown 链接 → deterministic edges | document evidence / LightRAG 旁路(勿与笔记反链混为同一产品入口) |
|
||||||
|
| 思源边界 | 借产品闭环,不借 `.sy` / SQL 真相 | `design/09-siyuan-reference/...` |
|
||||||
|
|
||||||
|
### 2.2 缺口(多真源风险点)
|
||||||
|
|
||||||
|
1. **编辑器层**:page_ref 降级为普通 link mark,无独立 inline atom;`[[` 输入规则 / 页面搜索插入不完整(legacy editor 仅插 `[[page]]` 占位)。
|
||||||
|
2. **索引层**:`extract_backlinks` 存的是**字符串目标**(路径 / wikilink 文本),未统一解析为稳定 `documentId` / ObjectIdentity;重命名后易漂。
|
||||||
|
3. **Kernel 层**:`References` / `BacklinksTo` 已声明,**未**成为 page_aggregate / 反链 UI 的正式 query 面。
|
||||||
|
4. **UI 层**:`collapseBacklinks` 明确 **「待接线:当前 Rust 壳未挂回链面板」**;旧 local-index 反链 DOM 已故意不复活(避免与 LightRAG 资料库入口混淆)。
|
||||||
|
5. **语义层**:E28 `@mention` 已暂停且纠偏为「人/会议」,**不是**页面引用;页面引用应走 `[[` / slash「页面」/ 转 page_reference,勿与 `@` 混用。
|
||||||
|
6. **块引用**:`((block))` 语法与类型有,主编辑岛与稳定 blockId 持久化尚未闭环。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 产品语义(对齐思源 / Wolai,落 MNote 边界)
|
||||||
|
|
||||||
|
### 3.1 三种引用(必须分开)
|
||||||
|
|
||||||
|
| 种类 | 用户形态 | 持久化 | 索引边 | 优先级 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| **页面引用** | `[[Title]]` / `[[path\|显示名]]` / page_reference 块 | Markdown 或 block props | `references` → page | **P0** |
|
||||||
|
| **块引用** | `((blockId))` | 需稳定 blockId | `references` → block | P1 |
|
||||||
|
| **外链 / 附件** | `[x](url)` / 资源 path | 普通 link / resource_ref | 不进「笔记反链」主列表(可进 resourceRefs) | 已有,保持 |
|
||||||
|
|
||||||
|
### 3.2 「双向」的真正含义
|
||||||
|
|
||||||
|
- 用户只写**正向**引用。
|
||||||
|
- 系统维护 `source → target` 边。
|
||||||
|
- **反链** = `target ← sources` 的投影查询,不是第二份手写列表。
|
||||||
|
- 图谱 / 搜索 / AI 上下文 / 导图挂件 **只读同一套边**,禁止各自扫一遍 Markdown 各存一份。
|
||||||
|
|
||||||
|
### 3.3 真源顺序(硬规则)
|
||||||
|
|
||||||
|
```
|
||||||
|
本地 .md(+ 将来稳定 block 元数据)
|
||||||
|
↓ 解析 / 保存
|
||||||
|
Page Aggregate blocks(含 page_reference / 将来 inline ref)
|
||||||
|
↓ 抽出
|
||||||
|
Reference edges(kernel 或 local-index 统一表,单一实现)
|
||||||
|
↓ 投影
|
||||||
|
反链面板 / search / AI tools / 可选图谱
|
||||||
|
```
|
||||||
|
|
||||||
|
**禁止**:
|
||||||
|
|
||||||
|
- 浏览器 localStorage 存链接图
|
||||||
|
- TipTap JSON 与 Markdown 各维护一套「官方」反链
|
||||||
|
- LightRAG entity 边冒充笔记 page backlink(可并列展示,须分入口)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 目标架构
|
||||||
|
|
||||||
|
### 4.1 引用边最小模型
|
||||||
|
|
||||||
|
```text
|
||||||
|
ReferenceEdge {
|
||||||
|
source_document_id: String, // local-md:… 或稳定 id
|
||||||
|
source_block_id: Option<String>,
|
||||||
|
target_kind: page | block | resource | external,
|
||||||
|
target_document_id: Option<String>, // 解析成功时
|
||||||
|
target_raw: String, // 原文 token / path,解析失败仍可展示
|
||||||
|
anchor_text: Option<String>,
|
||||||
|
locator: Option<String>, // 行号 / offset,便于跳转
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **写入**:文档保存成功、local-index refresh、watcher 变更后 **整页替换**该 source 的出边(思源式:按 root 删再插,避免脏边)。
|
||||||
|
- **读取**:
|
||||||
|
- `query.backlinks_to(documentId)` → 谁链到我
|
||||||
|
- `query.references_from(documentId)` → 我链向谁
|
||||||
|
- 与已有 API 对齐:先让 `GET /api/search/local-index/backlinks` **成为**上述 query 的 HTTP 面,再视需要升为 kernel query 名 `search.local_index.backlinks` → 最终 `kernel.backlinks_to`。
|
||||||
|
|
||||||
|
### 4.2 目标解析(消灭字符串多真源)
|
||||||
|
|
||||||
|
解析顺序建议:
|
||||||
|
|
||||||
|
1. 已是 `local-md:…` / 已知 documentId
|
||||||
|
2. 相对路径相对**当前文件** resolve → 规范化相对 root → `local-md:{encode}`
|
||||||
|
3. wikilink `[[Title]]`:按标题 / 文件名 stem 在索引内消歧(多命中则 raw 保留 + UI 标黄)
|
||||||
|
4. 失败:`target_document_id = null`,`target_raw` 保留,反链仍可按 raw 弱匹配(可选)
|
||||||
|
|
||||||
|
重命名 / 移动:走现有 tree command;索引 rebuild 时重算边(P0 可全量 rebuild;P1 增量 + transferBlockRef 类迁移可参考思源 `transferBlockRef`,**不**照搬 SQL)。
|
||||||
|
|
||||||
|
### 4.3 TipTap 显示层(v2 即可)
|
||||||
|
|
||||||
|
**不要**把 Notion registry 的「用户 Mention」直接当页面引用。
|
||||||
|
|
||||||
|
推荐两档:
|
||||||
|
|
||||||
|
| 档 | 形态 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **A(最小,先做)** | 保持 `paragraph` + `link` mark + `mnote-page-block-link`,补强 round-trip 与点击打开 | 已有主链,改动小 |
|
||||||
|
| **B(产品完整)** | 新增 inline atom:`pageMention` / 或专用 mark attrs:`data-mnote-ref="page"` + `data-target-id` | 便于样式、未解析态、禁止拆字 |
|
||||||
|
|
||||||
|
输入:
|
||||||
|
|
||||||
|
- InputRule:`[[` 打开页面搜索(复用 local search / tree query,**不是** `@`)
|
||||||
|
- Slash:插入「页面引用」
|
||||||
|
- 块菜单:段落 ↔ page_reference 转换(已有 `replace_block_with_page_reference` 可扩)
|
||||||
|
|
||||||
|
Markdown 导出:
|
||||||
|
|
||||||
|
- 优先 `[[target|label]]` 或 `[label](rel/path.md)`(**工作区内统一一种**;建议 **wikilink 作规范、md link 作兼容导入**)
|
||||||
|
- `mnote-editor-core` 已用 `[[id|label]]`,local-folder 路径型文档应导出 **相对路径** 以便 Git 可读:`[[docs/child|子页]]` 或 `[子页](docs/child.md)` — **实现前定一种 SSOT 写进本文件 §6**。
|
||||||
|
|
||||||
|
### 4.4 反链 UI
|
||||||
|
|
||||||
|
- 挂在**页面底部或右侧信息区**(Wolai/思源心智),**不要**塞回已移除的 local-index 设置页。
|
||||||
|
- 消费:`backlinks` API + Page Aggregate 选项 `collapseBacklinks`。
|
||||||
|
- 每条:来源标题、路径、可选 snippet、点击 `open document`(带 sourceKind/rootUri)。
|
||||||
|
- `showBlockRefCount`:有块引用后再接线。
|
||||||
|
|
||||||
|
### 4.5 与 AI / LightRAG
|
||||||
|
|
||||||
|
| 需求 | 入口 |
|
||||||
|
| --- | --- |
|
||||||
|
| 「哪些笔记链到这篇」 | 笔记反链 API / kernel edges |
|
||||||
|
| 「资料库里实体/PDF 证据」 | LightRAG / knowledge-rag |
|
||||||
|
| Agent 写引用 | 必须写 Markdown/block 合法引用,再走同一抽取器 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 分阶段落地(可执行)
|
||||||
|
|
||||||
|
### Phase 0 — 契约冻结(0.5–1d)
|
||||||
|
|
||||||
|
- [ ] 选定工作区持久化规范:`[[rel/path|label]]` **或** `[label](rel.md)` 为写出 SSOT(导入两者都认)。
|
||||||
|
- [ ] 固定 `ReferenceEdge` 字段与 `documentId` 解析顺序(§4.2)。
|
||||||
|
- [ ] 明确 `@` ≠ 页面引用(文档 + slash 文案)。
|
||||||
|
|
||||||
|
### Phase 1 — 索引单真源(1–2d)**【优先】**
|
||||||
|
|
||||||
|
- [ ] 强化 `extract_backlinks`:输出结构化 edge(含 resolved id),不只是字符串列表。
|
||||||
|
- [ ] `query_local_backlinks` 按 **resolved documentId** 聚合;兼容旧字符串。
|
||||||
|
- [ ] 单测:A→B 的 `[[B]]` 与 `[B](b.md)` 反链一致;重命名后 refresh 更新。
|
||||||
|
- [ ] 保持 `task452` 语义:API 可用;不恢复旧 settings DOM。
|
||||||
|
|
||||||
|
### Phase 2 — 反链面板(1–2d)
|
||||||
|
|
||||||
|
- [ ] 文档页挂载 backlinks panel(读 Phase 1 API)。
|
||||||
|
- [ ] 接通 `collapseBacklinks`。
|
||||||
|
- [ ] Smoke:两篇本地 md 互链,B 页可见 A。
|
||||||
|
|
||||||
|
### Phase 3 — 编辑器插入体验(2–3d)
|
||||||
|
|
||||||
|
- [ ] `[[` 触发页面搜索 + 插入规范 token / page_reference。
|
||||||
|
- [ ] 保存后索引增量/全量更新边。
|
||||||
|
- [ ] 未解析目标的视觉态(可选)。
|
||||||
|
- [ ] **仍用 TipTap 2.27.2**;不夹带 v3。
|
||||||
|
|
||||||
|
### Phase 4 — Kernel 边对齐(按主线空档)
|
||||||
|
|
||||||
|
- [ ] local-index 实现升为 `KernelEdgeType::References` 的唯一写入路径(或薄封装)。
|
||||||
|
- [ ] `BacklinksTo` 作为派生查询,不双写。
|
||||||
|
- [ ] Page Aggregate 可附带 `backlinks` 摘要(注意体积与权限)。
|
||||||
|
|
||||||
|
### Phase 5 — 块引用(后置)
|
||||||
|
|
||||||
|
- [ ] 稳定 blockId(与 UniqueID / 导出策略一起设计)。
|
||||||
|
- [ ] `((id))` 解析、预览、transfer on move。
|
||||||
|
- [ ] `showBlockRefCount`。
|
||||||
|
|
||||||
|
### 明确不做(本 v1)
|
||||||
|
|
||||||
|
- 为双向链接升级 TipTap 3
|
||||||
|
- 复制思源 `refs` SQL 表为产品真相
|
||||||
|
- 用 LightRAG 替代笔记反链
|
||||||
|
- 恢复旧 local-index 设置页反链 DOM
|
||||||
|
- 把 `@` 改成页面引用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 待决事项(实现前二选一写死)
|
||||||
|
|
||||||
|
1. **写出格式 SSOT**
|
||||||
|
- 方案 W:wikilink `[[rel/path|label]]`(贴近思源/Obsidian,editor-core 已有)
|
||||||
|
- 方案 M:标准 md link `[label](rel.md)`(Git 友好、通用)
|
||||||
|
- **建议**:索引与解析两者都认;**新插入默认 W**,导出可配置(默认 W)。
|
||||||
|
|
||||||
|
2. **page_reference 块 vs inline**
|
||||||
|
- 块级:独占一段(当前)
|
||||||
|
- 行内:段落中的 mention atom
|
||||||
|
- **建议**:P0 块级 + link mark;P1 行内 atom(真正「文中引用」)。
|
||||||
|
|
||||||
|
3. **反链面板位置**
|
||||||
|
- 页底固定区 vs 右侧 drawer
|
||||||
|
- **建议**:页底折叠区(`collapseBacklinks` 已存在语义)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 参考索引
|
||||||
|
|
||||||
|
| 来源 | 用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| 思源 `kernel/sql` `refs` 表 / `block_ref_query.go` | 边模型、按 root 重建、反链查询形状 |
|
||||||
|
| 思源 conf `Backlink*` / `VirtualBlockRef` | 产品选项;虚拟引用可后置 |
|
||||||
|
| `design/09-siyuan-reference/...` | 借鉴边界 |
|
||||||
|
| `design/01-tree-first-graph-kernel/...` | tree + references 边 |
|
||||||
|
| leptos-tiptap `tiptap_link.ts` | link mark bridge |
|
||||||
|
| notion-like `mention-dropdown-menu` | 仅 suggestion UI 模式 |
|
||||||
|
| `local_search_index.rs` `extract_backlinks` | 现抽取实现 |
|
||||||
|
| `sidebar-page-settings-runtime.js` | collapseBacklinks 待接线文案 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 验收标准(Phase 1–2 最小闭环)
|
||||||
|
|
||||||
|
1. 在 root 下创建 `A.md`,正文写入指向 `B.md` 的规范引用并保存。
|
||||||
|
2. 打开 `B`:反链面板出现 `A`(标题可点,带 local source 参数)。
|
||||||
|
3. 删除 A 中引用并保存 / 刷新索引后,B 反链消失。
|
||||||
|
4. 同一套数据:`GET .../local-index/backlinks` 与面板一致。
|
||||||
|
5. 无第二套前端链接缓存;无 TipTap 3 升级。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 与主线关系
|
||||||
|
|
||||||
|
当前主线仍是 Page Aggregate / tree / local-first。双向链接应:
|
||||||
|
|
||||||
|
- **先**吃透已有 local-index + page_reference(Phase 1–2)
|
||||||
|
- **再**编辑体验(Phase 3)
|
||||||
|
- Kernel 图边与块引用跟 tree-first 空档合并,避免另起存储
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文是 process 设计;落地时按 Phase 开 checklist / smoke,完成后迁 `done/`。*
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# 7-77 mnote-e2e 知识中枢架构 v1
|
||||||
|
|
||||||
|
> 状态:`process` · 可执行设计(落地 skill 前的 SSOT)
|
||||||
|
> 范围:AI 主体 `mnote-e2e` 工作区 + 多 AI / Admin 共用的**系统·项目知识中枢**
|
||||||
|
> 非目标:替换 LightRAG;替换 vault 读密;替代全局 self-improving 记忆循环
|
||||||
|
> 前置:Web PAT(笔记 CRUD)与 vault mnv1(读密)已分职;本设计只落**可读 Markdown 真源**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 问题与目标
|
||||||
|
|
||||||
|
### 1.1 要解决什么
|
||||||
|
|
||||||
|
- 重复查 skill 路径、服务器 URL、运维约定;
|
||||||
|
- 系统级配置散落在聊天 / 临时文件里;
|
||||||
|
- 项目上下文与全局基建混杂,难给 **Admin 复核** 与 **多 AI 共用**。
|
||||||
|
|
||||||
|
### 1.2 产品定位
|
||||||
|
|
||||||
|
**知识中枢 = 系统重要信息 + 项目重要信息的「索引 · 导航 · 积累」层。**
|
||||||
|
Admin 与 AI 共用同一套 Markdown 树;LightRAG 是可选增强,不是唯一入口。
|
||||||
|
|
||||||
|
### 1.3 边界
|
||||||
|
|
||||||
|
| 概念 | 本中枢关系 |
|
||||||
|
|------|------------|
|
||||||
|
| self-improving-for-codex | 不替代;行为记忆仍在 agent 侧 |
|
||||||
|
| LightRAG | 可选索引 `00-system` + 显式项目;排除 inbox/archive/系统点目录 |
|
||||||
|
| vault / mnv1 | 正文禁止密文;只写 credential 指针 |
|
||||||
|
| Web PAT | e2e 工作区 CRUD 通道 |
|
||||||
|
| 全局 skill / AGENTS | 工作区 `AGENTS.md` 强制「先查中枢」 |
|
||||||
|
|
||||||
|
### 1.4 成功标准
|
||||||
|
|
||||||
|
1. Admin 打开 e2e 工作区根即可浏览系统/项目知识。
|
||||||
|
2. AI 默认先读 `INDEX.md` 再动手。
|
||||||
|
3. 秘密只在 vault。
|
||||||
|
4. Admin 可清 inbox、删冗余、批注待更新。
|
||||||
|
5. 结构稳定到可做成 skill。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 真源与放置
|
||||||
|
|
||||||
|
### 2.1 工作区
|
||||||
|
|
||||||
|
```text
|
||||||
|
主体:mnote-e2e(ai_service)
|
||||||
|
根: /mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space
|
||||||
|
鉴权:Web PAT notes.* + tree.*
|
||||||
|
读密:mnv1(与中枢分离)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 中枢根 = 工作区根(无额外 00-hub 层)
|
||||||
|
|
||||||
|
**决策(2026-07-28)**:不再套 `00-hub/`。e2e 工作区几乎整仓即知识中枢,多一层路径长、心智重复。
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-space/ # ← 中枢根
|
||||||
|
AGENTS.md # 工作区纪律(启动入口)
|
||||||
|
README.md # 中枢是什么、怎么用
|
||||||
|
INDEX.md # 总索引(启动必读,短)
|
||||||
|
00-system/ # 通用 / 系统级
|
||||||
|
10-projects/ # 分项目
|
||||||
|
90-inbox/ # AI 草稿,Admin 晋升
|
||||||
|
99-archive/ # 过期
|
||||||
|
.mnote/ ai-sessions/ … # 系统目录:不进索引、不进 RAG 默认范围
|
||||||
|
```
|
||||||
|
|
||||||
|
**约定**
|
||||||
|
|
||||||
|
| 类别 | 路径 | 是否知识树 |
|
||||||
|
|------|------|------------|
|
||||||
|
| 知识正文 | `INDEX.md`、`00-system/**`、`10-projects/**`、`90-inbox/**`、`99-archive/**` | 是 |
|
||||||
|
| 系统运行时 | `.mnote/**`、`ai-sessions/**`、`.reasonix/**`、`.codegraph/**` | **否** |
|
||||||
|
|
||||||
|
> 实测:e2e PAT 已验证 tree 创建/重命名/删除 + `resource/read|write`;用户试探文档已清理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 信息架构
|
||||||
|
|
||||||
|
### 3.1 分层纪律(必守)
|
||||||
|
|
||||||
|
| 层 | 放什么 | 不放什么 |
|
||||||
|
|----|--------|----------|
|
||||||
|
| **`00-system`** | **主机与基建**:Kubuntu、OrangeVPS、heyun;VPN/frp;全机 skill;系统凭证指针 | MNote 应用账号(mnote-e2e 等)、产品端口 3000/3003、PAT/mnv1 产品细则 |
|
||||||
|
| **`10-projects/*`** | 产品与项目:身份、业务端口、架构、runbook | 把 heyun/Kubuntu 主机表重复抄一套当「项目」而不链系统层 |
|
||||||
|
|
||||||
|
### 3.2 `00-system/`(系统 / 主机)
|
||||||
|
|
||||||
|
```text
|
||||||
|
00-system/
|
||||||
|
INDEX.md
|
||||||
|
01-hosts/ # Kubuntu / OrangeVPS / heyun(可达性备注)
|
||||||
|
02-skills/ # 全机 skill 登记
|
||||||
|
03-network/ # 代理 profile、frp 约定
|
||||||
|
04-endpoints/ # 系统向入口(代理/面板);产品入口链到项目
|
||||||
|
05-repos/ # 本机仓库与数据路径
|
||||||
|
06-agents-runtime/ # 本机 agent 宿主
|
||||||
|
07-credentials-map/# 系统凭证指针(无密文)
|
||||||
|
08-ops-runbooks/ # 主机/网络运维
|
||||||
|
09-governance/ # 中枢治理
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 `10-projects/<slug>/`
|
||||||
|
|
||||||
|
```text
|
||||||
|
10-projects/
|
||||||
|
INDEX.md
|
||||||
|
mnote-core/ # 含 identity.md、endpoints.md(产品端口)
|
||||||
|
ai-runtime/ # 含 credentials.md(PAT/mnv1/AI 密码本指针)
|
||||||
|
infra-ops/ # 部署视图;主机真源仍链 00-system/01-hosts
|
||||||
|
<slug>/
|
||||||
|
README.md INDEX.md architecture.md runbook.md
|
||||||
|
decisions.md glossary.md open-questions.md links.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 inbox / archive
|
||||||
|
|
||||||
|
| 区 | 写 | 默认启动读 |
|
||||||
|
|----|----|------------|
|
||||||
|
| `90-inbox/` | AI 草稿 | 否 |
|
||||||
|
| `99-archive/` | 归档 | 否 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 安全与索引纪律
|
||||||
|
|
||||||
|
1. 禁止 password / apikey / cookie / mnpat1 / mnv1 明文。
|
||||||
|
2. 凭证只写:subject、credentialId、用途、字段名。
|
||||||
|
3. 启动只读:`AGENTS.md` → `INDEX.md` → 按需子 INDEX。
|
||||||
|
4. 清单用表格 + 链接;明细按需打开。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 角色
|
||||||
|
|
||||||
|
| 角色 | 读 | 写 | 删 system 核心 |
|
||||||
|
|------|----|----|----------------|
|
||||||
|
| Admin | 全部 | 全部 | 可 |
|
||||||
|
| AI(e2e PAT) | 工作区知识树 | inbox + 受控更新 catalog | 禁止擅自删 |
|
||||||
|
| 其他用户 | 无 membership 则不可见 | — | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 后续接线
|
||||||
|
|
||||||
|
- 工作区 `AGENTS.md`:先查中枢、禁密文、inbox 规则。
|
||||||
|
- Skill:`$mnote-e2e-knowledge-hub`
|
||||||
|
- 真源:`~/.codex/skills/mnote-e2e-knowledge-hub/`
|
||||||
|
- Grok symlink:`~/.grok/skills/mnote-e2e-knowledge-hub`
|
||||||
|
- 含 worker 模板:`references/worker-prompts.md` · 结构:`references/structure.md`
|
||||||
|
- LightRAG:默认可挂 `00-system/**`;项目 opt-in;排除 inbox/archive/点目录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 落地阶段
|
||||||
|
|
||||||
|
| 阶段 | 内容 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| A | 以 e2e 根建树 + 占位 INDEX | **done** |
|
||||||
|
| B | 填 skills/servers/identity 实表(kb2/kb3 充实) | **done**(可持续更新) |
|
||||||
|
| C | skill + 完善 AGENTS.md | **done**(2026-07-29) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 反模式
|
||||||
|
|
||||||
|
- 密文进 Markdown
|
||||||
|
- 每 AI 各建一套 hub
|
||||||
|
- 把 `ai-sessions` 当知识
|
||||||
|
- 镜像整个 git 仓库
|
||||||
|
- AI 静默删 `00-system` 核心页
|
||||||
|
- ~~多包一层无必要的 `00-hub/`~~(已否决)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 开放问题
|
||||||
|
|
||||||
|
1. skill 登记:半自动扫盘 + Admin 审?
|
||||||
|
2. `design/**` 以 pointer 链入(推荐)还是摘要复制?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更记录
|
||||||
|
|
||||||
|
| 日期 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 2026-07-28 | 初稿:含 `00-hub` 层 |
|
||||||
|
| 2026-07-28 | **改中枢根 = e2e 工作区根**,去掉 `00-hub`;并落地目录骨架 |
|
||||||
|
| 2026-07-28 | **纠正分层**:系统级 = 主机(Kubuntu/OrangeVPS/heyun)与基建;mnote-e2e 身份/产品端口下沉 `10-projects` |
|
||||||
|
| 2026-07-29 | 落地 skill `$mnote-e2e-knowledge-hub`(Codex 真源 + Grok 链接);阶段 B/C 收口 |
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# 23 · 退役产品全库清理清单 v1
|
||||||
|
|
||||||
|
> 状态:**in-progress → 高优先级 Waves A1–C 已落地(2026-07-28)**
|
||||||
|
> 日期:2026-07-28
|
||||||
|
> 目标:把 convex + Weknora + openhub + reasonix + Hermes + ACP + OpenCode + Sidex + Yuxi + legacy_next_compat + soft retirement 残留彻底扫光,像没有存在过一样。
|
||||||
|
> 优先级:**高**(编译不红、runtime 不暴露、文档不广告)→ **中**(调试残留)→ **低**(历史叙事、死 smoke)。
|
||||||
|
|
||||||
|
## 1. 整体盘点(更新 2026-07-28 执行后)
|
||||||
|
|
||||||
|
| 品牌 | 产品面残留 | 调试/测试面 | 历史叙事 | 风险 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **Hermes** | seed→`pi`;`HERMES_HOME` 兼容读已删;`AiRuntimeOwner::RustWebPi` | soft-retire smoke 保留 exit 0 | design/old 可留 | **已收** |
|
||||||
|
| **OpenHub / OpenCode** | 无产品入口 | soft-retire smoke | design/old | **低** |
|
||||||
|
| **Reasonix** | seed→`pi` | `.reasonix` 目录忽略保留 | design/old | **已收** |
|
||||||
|
| **ACP** | DB 列 `acp_*` 保留(schema);产品 runtime 无 ACP 宿主 | 极少 | | **中(列名兼容)** |
|
||||||
|
| **next_compat / LEGACY_NEXT** | AppConfig / proxy / documents Next 链已删 | smoke 已去 env | design/done 历史稿 | **已收** |
|
||||||
|
| **convex_retired** | 已扫完 | | | **已完** |
|
||||||
|
| **Weknora / Yuxi / Sidex** | 极少 | | design/old | **低** |
|
||||||
|
|
||||||
|
## 2. 本批已执行(A1 → A2 → A3 → B → C)
|
||||||
|
|
||||||
|
### A1 · control-plane brand → pi ✅
|
||||||
|
- sqlite / turso seed:`hermes`/`reasonix` → `pi`,display「我的 Pi」
|
||||||
|
- 迁移:历史 `agent_id='hermes'` 就地 UPDATE 到 `pi`
|
||||||
|
- fixtures / `turso_store` / `control-plane-dev-seed.js` 字面量中性化
|
||||||
|
|
||||||
|
### A2 · LEGACY_NEXT / documents proxy ✅
|
||||||
|
- `AppConfig` 删除 `legacy_next_base_url` / `enable_legacy_next_compat` / `compat_next_base_path`
|
||||||
|
- `session` / `gateway` health 与 legacy proxy 删除
|
||||||
|
- `documents` Next proxy 栈删除
|
||||||
|
- OnlyOffice 非 local writeback → fail-closed(`onlyoffice_cloud_writeback_unavailable`)
|
||||||
|
- 保留 OnlyOffice **同源 server proxy** 所需 `append_path_and_query`
|
||||||
|
|
||||||
|
### A3 · Hermes 命名 / HERMES_HOME ✅
|
||||||
|
- `AiRuntimeOwner::RustWebHermes` → `RustWebPi`
|
||||||
|
- spike:`open_hermes_page_ai_drawer` → `open_page_ai_drawer`
|
||||||
|
- `agent_profile_home`:仅 `MNOTE_AGENT_HOME` / `~/.mnote-agent`(去掉 `HERMES_HOME` / `~/.hermes`)
|
||||||
|
- audit log:仅 `MNOTE_AGENT_TOOL_AUDIT_LOG`
|
||||||
|
- AGENTS.md:去掉「旧 agent 网关 / OpenCode 代理壳」宣传句
|
||||||
|
- `.env.all`:删除 `MNOTE_HERMES_API_*`
|
||||||
|
|
||||||
|
### B · compat 410 / 文案 / debug ✅
|
||||||
|
- `/api/ai-agent/run` 继续 **410** `legacy_ai_agent_run_retired`(含 hermes/codex provider 负向合同)
|
||||||
|
- compat 测试去 mock Next 上游,只断言 GONE
|
||||||
|
- debug shell:`MNOTE_WEB_ENABLE_DEBUG_SHELL_ROUTES` 默认 false(边界保持)
|
||||||
|
|
||||||
|
### C · smoke / 编译验证 ✅
|
||||||
|
- 17 个 smoke 去掉 `MNOTE_WEB_*LEGACY_NEXT*`
|
||||||
|
- `task164` 断言改为不出现 `legacy_next_compat`
|
||||||
|
- 死 hermes/opencode smoke 已 soft-retire(Wave 10,保持)
|
||||||
|
- 验证:
|
||||||
|
- `cargo check -p mnote-web --tests` 无 error
|
||||||
|
- `cargo test -p bridge-runtime --lib` 116 passed
|
||||||
|
- `cargo test -p control-plane --lib` 37 passed
|
||||||
|
- `cargo test -p mnote-web --lib routes::compat` 4 passed
|
||||||
|
|
||||||
|
## 3. 仍可后续(非阻塞)
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| ACP 列名 `acp_*` | 控制面 schema 兼容;改列需独立迁移批 |
|
||||||
|
| actor 占位拒绝 `hermes` | `mnote_tools` 仍拒绝 actor=`hermes` 占位 id(负向安全) |
|
||||||
|
| `mnote-hermes-tool` source | document-session 历史 external write source 兼容读 |
|
||||||
|
| `.reasonix` / `.opencode` 目录忽略 | local_folder 扫描忽略,非产品入口 |
|
||||||
|
| design/old 历史稿 | 不阻塞产品面 |
|
||||||
|
| 测试函数名 `hermes_tools_*` 前缀 | 历史命名,schema 已中性化 |
|
||||||
|
|
||||||
|
## 4. 验收 Checklist
|
||||||
|
|
||||||
|
- [x] `cargo check -p mnote-web --tests` 无 error
|
||||||
|
- [x] `cargo test -p bridge-runtime --lib` 全通过
|
||||||
|
- [x] `cargo test -p control-plane --lib` 全通过
|
||||||
|
- [x] AGENTS.md 不再把退役 agent 宿主写成可选主链
|
||||||
|
- [x] smoke 不再注入 `MNOTE_WEB_*LEGACY_NEXT*`
|
||||||
|
- [x] 产品 profile home 不再读 `HERMES_HOME` / `~/.hermes`
|
||||||
|
- [ ] (可选)ARCHITECTURE.md 全文扫 brand 叙事
|
||||||
|
- [ ] (可选)ACP 列 rename 迁移批
|
||||||
|
|
||||||
|
## 5. 工具回扫命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "legacy_next|LEGACY_NEXT|HERMES_HOME|RustWebHermes|enable_legacy_next" rust scripts --glob '!**/target/**'
|
||||||
|
rg -n "MNOTE_WEB_ENABLE_LEGACY_NEXT|MNOTE_HERMES" scripts .env.all
|
||||||
|
cargo check -p mnote-web --tests
|
||||||
|
```
|
||||||
@@ -50,7 +50,7 @@ pub struct OnlyOfficeProxyPreparationInput {
|
|||||||
pub supabase_url: Option<String>,
|
pub supabase_url: Option<String>,
|
||||||
pub supabase_internal_url: Option<String>,
|
pub supabase_internal_url: Option<String>,
|
||||||
pub onlyoffice_storage_host_override: Option<String>,
|
pub onlyoffice_storage_host_override: Option<String>,
|
||||||
pub convex_origin: Option<String>,
|
pub extra_origin: Option<String>,
|
||||||
pub supabase_anon_key: Option<String>,
|
pub supabase_anon_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ pub fn prepare_proxy_request(
|
|||||||
let supa = try_parse_origin_host(input.supabase_url.as_deref());
|
let supa = try_parse_origin_host(input.supabase_url.as_deref());
|
||||||
let supa_internal_origin = try_parse_origin_url(input.supabase_internal_url.as_deref());
|
let supa_internal_origin = try_parse_origin_url(input.supabase_internal_url.as_deref());
|
||||||
let storage_override = try_parse_origin_host(input.onlyoffice_storage_host_override.as_deref());
|
let storage_override = try_parse_origin_host(input.onlyoffice_storage_host_override.as_deref());
|
||||||
let convex_origin = try_parse_origin_url(input.convex_origin.as_deref());
|
let extra_origin = try_parse_origin_url(input.extra_origin.as_deref());
|
||||||
|
|
||||||
let is_supabase_path = target.path().starts_with("/storage/v1/")
|
let is_supabase_path = target.path().starts_with("/storage/v1/")
|
||||||
|| target.path().starts_with("/auth/v1/")
|
|| target.path().starts_with("/auth/v1/")
|
||||||
@@ -302,7 +302,7 @@ pub fn prepare_proxy_request(
|
|||||||
add_allowed(hostname, port);
|
add_allowed(hostname, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(origin) = convex_origin.as_ref() {
|
if let Some(origin) = extra_origin.as_ref() {
|
||||||
let port = origin
|
let port = origin
|
||||||
.port()
|
.port()
|
||||||
.map(|value| value.to_string())
|
.map(|value| value.to_string())
|
||||||
@@ -792,7 +792,7 @@ mod tests {
|
|||||||
supabase_url: Some("https://public.example.com".into()),
|
supabase_url: Some("https://public.example.com".into()),
|
||||||
supabase_internal_url: Some("http://127.0.0.1:18000".into()),
|
supabase_internal_url: Some("http://127.0.0.1:18000".into()),
|
||||||
onlyoffice_storage_host_override: None,
|
onlyoffice_storage_host_override: None,
|
||||||
convex_origin: Some("http://127.0.0.1:3210".into()),
|
extra_origin: Some("http://127.0.0.1:3210".into()),
|
||||||
supabase_anon_key: Some("anon".into()),
|
supabase_anon_key: Some("anon".into()),
|
||||||
})
|
})
|
||||||
.expect("proxy should prepare");
|
.expect("proxy should prepare");
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
-- 002-ai-runtime-store.sql
|
-- 002-ai-runtime-store.sql
|
||||||
-- MNote ACP/Hermes runtime session store.
|
-- MNote AI runtime session store.
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ai_runtime_runs (
|
CREATE TABLE IF NOT EXISTS ai_runtime_runs (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
|||||||
@@ -591,7 +591,7 @@ fn list_ai_agent_profile_access_rows(
|
|||||||
g.can_manage_config, g.created_at, g.updated_at, g.revision
|
g.can_manage_config, g.created_at, g.updated_at, g.revision
|
||||||
FROM ai_agent_profiles p
|
FROM ai_agent_profiles p
|
||||||
JOIN ai_agent_profile_grants g ON g.profile_id = p.id
|
JOIN ai_agent_profile_grants g ON g.profile_id = p.id
|
||||||
WHERE p.agent_id = 'hermes'
|
WHERE p.agent_id = 'pi'
|
||||||
AND p.status = 'active'
|
AND p.status = 'active'
|
||||||
AND g.can_run = 1
|
AND g.can_run = 1
|
||||||
AND (
|
AND (
|
||||||
@@ -2002,12 +2002,24 @@ impl ControlPlaneStore for SqliteControlPlaneStore {
|
|||||||
}
|
}
|
||||||
let conn = self.lock_conn()?;
|
let conn = self.lock_conn()?;
|
||||||
let now = now_text();
|
let now = now_text();
|
||||||
|
// 退役 brand:历史 hermes agent_id 就地迁到 pi,避免 UNIQUE 下再插一套孤儿行。
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE ai_agent_profiles
|
||||||
|
SET agent_id = 'pi',
|
||||||
|
display_name = CASE
|
||||||
|
WHEN display_name = '我的 Hermes' THEN '我的 Pi'
|
||||||
|
ELSE display_name
|
||||||
|
END,
|
||||||
|
updated_at = ?1
|
||||||
|
WHERE agent_id = 'hermes'",
|
||||||
|
params![now],
|
||||||
|
)?;
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO ai_agent_profiles (
|
"INSERT INTO ai_agent_profiles (
|
||||||
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
||||||
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
||||||
)
|
)
|
||||||
VALUES ('shared_lite', 'hermes', 'shared', '', 'lite', 'lite', 'Lite', 'active', ?1, ?2, 1)
|
VALUES ('shared_lite', 'pi', 'shared', '', 'lite', 'lite', 'Lite', 'active', ?1, ?2, 1)
|
||||||
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
||||||
DO UPDATE SET status = 'active', updated_at = excluded.updated_at",
|
DO UPDATE SET status = 'active', updated_at = excluded.updated_at",
|
||||||
params![now, now],
|
params![now, now],
|
||||||
@@ -2093,7 +2105,7 @@ impl ControlPlaneStore for SqliteControlPlaneStore {
|
|||||||
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
||||||
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
||||||
)
|
)
|
||||||
VALUES (?1, 'hermes', 'shared', '', ?2, ?3, ?4, 'active', ?5, ?6, 1)
|
VALUES (?1, 'pi', 'shared', '', ?2, ?3, ?4, 'active', ?5, ?6, 1)
|
||||||
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
||||||
DO UPDATE SET status = 'active', display_name = excluded.display_name,
|
DO UPDATE SET status = 'active', display_name = excluded.display_name,
|
||||||
isolated_profile_name = excluded.isolated_profile_name, updated_at = excluded.updated_at",
|
isolated_profile_name = excluded.isolated_profile_name, updated_at = excluded.updated_at",
|
||||||
@@ -2121,9 +2133,10 @@ impl ControlPlaneStore for SqliteControlPlaneStore {
|
|||||||
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
||||||
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
||||||
)
|
)
|
||||||
VALUES (?1, 'hermes', 'personal', ?2, 'default', ?3, '我的 Hermes', 'active', ?4, ?5, 1)
|
VALUES (?1, 'pi', 'personal', ?2, 'default', ?3, '我的 Pi', 'active', ?4, ?5, 1)
|
||||||
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
||||||
DO UPDATE SET status = 'active', updated_at = excluded.updated_at",
|
DO UPDATE SET status = 'active', display_name = excluded.display_name,
|
||||||
|
isolated_profile_name = excluded.isolated_profile_name, updated_at = excluded.updated_at",
|
||||||
params![personal_profile_id, user_id, personal_profile_name, now, now],
|
params![personal_profile_id, user_id, personal_profile_name, now, now],
|
||||||
)?;
|
)?;
|
||||||
let personal_grant_id = format!("grant_{personal_profile_id}_owner");
|
let personal_grant_id = format!("grant_{personal_profile_id}_owner");
|
||||||
@@ -4461,8 +4474,8 @@ mod tests {
|
|||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
title: Some("初始标题".to_string()),
|
title: Some("初始标题".to_string()),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
trace_id: Some("trace_1".to_string()),
|
trace_id: Some("trace_1".to_string()),
|
||||||
status: "running".to_string(),
|
status: "running".to_string(),
|
||||||
runtime_json: "{\"status\":\"running\"}".to_string(),
|
runtime_json: "{\"status\":\"running\"}".to_string(),
|
||||||
@@ -4480,8 +4493,8 @@ mod tests {
|
|||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
title: Some("更新标题".to_string()),
|
title: Some("更新标题".to_string()),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
trace_id: Some("trace_2".to_string()),
|
trace_id: Some("trace_2".to_string()),
|
||||||
status: "completed".to_string(),
|
status: "completed".to_string(),
|
||||||
runtime_json: "{\"status\":\"completed\"}".to_string(),
|
runtime_json: "{\"status\":\"completed\"}".to_string(),
|
||||||
@@ -4511,8 +4524,8 @@ mod tests {
|
|||||||
document_id: Some("doc_1".to_string()),
|
document_id: Some("doc_1".to_string()),
|
||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
event_type: "message.delta".to_string(),
|
event_type: "message.delta".to_string(),
|
||||||
payload_json: "{\"text\":\"hello\"}".to_string(),
|
payload_json: "{\"text\":\"hello\"}".to_string(),
|
||||||
})
|
})
|
||||||
@@ -4526,8 +4539,8 @@ mod tests {
|
|||||||
document_id: Some("doc_1".to_string()),
|
document_id: Some("doc_1".to_string()),
|
||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
event_type: "run.completed".to_string(),
|
event_type: "run.completed".to_string(),
|
||||||
payload_json: "{\"status\":\"completed\"}".to_string(),
|
payload_json: "{\"status\":\"completed\"}".to_string(),
|
||||||
})
|
})
|
||||||
@@ -4639,8 +4652,8 @@ mod tests {
|
|||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
title: None,
|
title: None,
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
trace_id: None,
|
trace_id: None,
|
||||||
status: "completed".to_string(),
|
status: "completed".to_string(),
|
||||||
runtime_json: "{\"status\":\"completed\"}".to_string(),
|
runtime_json: "{\"status\":\"completed\"}".to_string(),
|
||||||
@@ -4963,7 +4976,7 @@ mod tests {
|
|||||||
workspace_id: Some("ws_1".to_string()),
|
workspace_id: Some("ws_1".to_string()),
|
||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: Some("run_2".to_string()),
|
run_id: Some("run_2".to_string()),
|
||||||
provider: "reasonix".to_string(),
|
provider: "pi".to_string(),
|
||||||
provider_session_id: None,
|
provider_session_id: None,
|
||||||
tool_name: "fs.write".to_string(),
|
tool_name: "fs.write".to_string(),
|
||||||
allowed: false,
|
allowed: false,
|
||||||
|
|||||||
@@ -1125,7 +1125,7 @@ fn list_ai_agent_profile_access_rows(
|
|||||||
g.can_manage_config, g.created_at, g.updated_at, g.revision
|
g.can_manage_config, g.created_at, g.updated_at, g.revision
|
||||||
FROM ai_agent_profiles p
|
FROM ai_agent_profiles p
|
||||||
JOIN ai_agent_profile_grants g ON g.profile_id = p.id
|
JOIN ai_agent_profile_grants g ON g.profile_id = p.id
|
||||||
WHERE p.agent_id = 'hermes'
|
WHERE p.agent_id = 'pi'
|
||||||
AND p.status = 'active'
|
AND p.status = 'active'
|
||||||
AND g.can_run = 1
|
AND g.can_run = 1
|
||||||
AND (
|
AND (
|
||||||
@@ -2530,12 +2530,24 @@ impl ControlPlaneStore for TursoControlPlaneStore {
|
|||||||
}
|
}
|
||||||
let conn = self.lock_conn()?;
|
let conn = self.lock_conn()?;
|
||||||
let now = now_text();
|
let now = now_text();
|
||||||
|
// 退役 brand:历史 hermes agent_id 就地迁到 pi,避免 UNIQUE 下再插一套孤儿行。
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE ai_agent_profiles
|
||||||
|
SET agent_id = 'pi',
|
||||||
|
display_name = CASE
|
||||||
|
WHEN display_name = '我的 Hermes' THEN '我的 Pi'
|
||||||
|
ELSE display_name
|
||||||
|
END,
|
||||||
|
updated_at = ?1
|
||||||
|
WHERE agent_id = 'hermes'",
|
||||||
|
params![now],
|
||||||
|
)?;
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO ai_agent_profiles (
|
"INSERT INTO ai_agent_profiles (
|
||||||
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
||||||
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
||||||
)
|
)
|
||||||
VALUES ('shared_lite', 'hermes', 'shared', '', 'lite', 'lite', 'Lite', 'active', ?1, ?2, 1)
|
VALUES ('shared_lite', 'pi', 'shared', '', 'lite', 'lite', 'Lite', 'active', ?1, ?2, 1)
|
||||||
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
||||||
DO UPDATE SET status = 'active', updated_at = excluded.updated_at",
|
DO UPDATE SET status = 'active', updated_at = excluded.updated_at",
|
||||||
params![now, now],
|
params![now, now],
|
||||||
@@ -2621,7 +2633,7 @@ impl ControlPlaneStore for TursoControlPlaneStore {
|
|||||||
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
||||||
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
||||||
)
|
)
|
||||||
VALUES (?1, 'hermes', 'shared', '', ?2, ?3, ?4, 'active', ?5, ?6, 1)
|
VALUES (?1, 'pi', 'shared', '', ?2, ?3, ?4, 'active', ?5, ?6, 1)
|
||||||
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
||||||
DO UPDATE SET status = 'active', display_name = excluded.display_name,
|
DO UPDATE SET status = 'active', display_name = excluded.display_name,
|
||||||
isolated_profile_name = excluded.isolated_profile_name, updated_at = excluded.updated_at",
|
isolated_profile_name = excluded.isolated_profile_name, updated_at = excluded.updated_at",
|
||||||
@@ -2649,9 +2661,10 @@ impl ControlPlaneStore for TursoControlPlaneStore {
|
|||||||
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
id, agent_id, profile_kind, owner_user_id, base_profile_name,
|
||||||
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
isolated_profile_name, display_name, status, created_at, updated_at, revision
|
||||||
)
|
)
|
||||||
VALUES (?1, 'hermes', 'personal', ?2, 'default', ?3, '我的 Hermes', 'active', ?4, ?5, 1)
|
VALUES (?1, 'pi', 'personal', ?2, 'default', ?3, '我的 Pi', 'active', ?4, ?5, 1)
|
||||||
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
|
||||||
DO UPDATE SET status = 'active', updated_at = excluded.updated_at",
|
DO UPDATE SET status = 'active', display_name = excluded.display_name,
|
||||||
|
isolated_profile_name = excluded.isolated_profile_name, updated_at = excluded.updated_at",
|
||||||
params![personal_profile_id, user_id, personal_profile_name, now, now],
|
params![personal_profile_id, user_id, personal_profile_name, now, now],
|
||||||
)?;
|
)?;
|
||||||
let personal_grant_id = format!("grant_{personal_profile_id}_owner");
|
let personal_grant_id = format!("grant_{personal_profile_id}_owner");
|
||||||
@@ -4891,8 +4904,8 @@ mod tests {
|
|||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
title: Some("初始标题".to_string()),
|
title: Some("初始标题".to_string()),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
trace_id: Some("trace_1".to_string()),
|
trace_id: Some("trace_1".to_string()),
|
||||||
status: "running".to_string(),
|
status: "running".to_string(),
|
||||||
runtime_json: "{\"status\":\"running\"}".to_string(),
|
runtime_json: "{\"status\":\"running\"}".to_string(),
|
||||||
@@ -4910,8 +4923,8 @@ mod tests {
|
|||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
title: Some("更新标题".to_string()),
|
title: Some("更新标题".to_string()),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
trace_id: Some("trace_2".to_string()),
|
trace_id: Some("trace_2".to_string()),
|
||||||
status: "completed".to_string(),
|
status: "completed".to_string(),
|
||||||
runtime_json: "{\"status\":\"completed\"}".to_string(),
|
runtime_json: "{\"status\":\"completed\"}".to_string(),
|
||||||
@@ -4941,8 +4954,8 @@ mod tests {
|
|||||||
document_id: Some("doc_1".to_string()),
|
document_id: Some("doc_1".to_string()),
|
||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
event_type: "message.delta".to_string(),
|
event_type: "message.delta".to_string(),
|
||||||
payload_json: "{\"text\":\"hello\"}".to_string(),
|
payload_json: "{\"text\":\"hello\"}".to_string(),
|
||||||
})
|
})
|
||||||
@@ -4956,8 +4969,8 @@ mod tests {
|
|||||||
document_id: Some("doc_1".to_string()),
|
document_id: Some("doc_1".to_string()),
|
||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
event_type: "run.completed".to_string(),
|
event_type: "run.completed".to_string(),
|
||||||
payload_json: "{\"status\":\"completed\"}".to_string(),
|
payload_json: "{\"status\":\"completed\"}".to_string(),
|
||||||
})
|
})
|
||||||
@@ -5005,8 +5018,8 @@ mod tests {
|
|||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
title: None,
|
title: None,
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
trace_id: None,
|
trace_id: None,
|
||||||
status: "completed".to_string(),
|
status: "completed".to_string(),
|
||||||
runtime_json: "{\"status\":\"completed\"}".to_string(),
|
runtime_json: "{\"status\":\"completed\"}".to_string(),
|
||||||
@@ -5471,7 +5484,7 @@ mod tests {
|
|||||||
workspace_id: Some("ws_1".to_string()),
|
workspace_id: Some("ws_1".to_string()),
|
||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: Some("run_2".to_string()),
|
run_id: Some("run_2".to_string()),
|
||||||
provider: "reasonix".to_string(),
|
provider: "pi".to_string(),
|
||||||
provider_session_id: None,
|
provider_session_id: None,
|
||||||
tool_name: "fs.write".to_string(),
|
tool_name: "fs.write".to_string(),
|
||||||
allowed: false,
|
allowed: false,
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ fn libsql_local_store_covers_control_plane_core_flows() {
|
|||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
title: Some("libSQL run".to_string()),
|
title: Some("libSQL run".to_string()),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
trace_id: Some("trace_1".to_string()),
|
trace_id: Some("trace_1".to_string()),
|
||||||
status: "running".to_string(),
|
status: "running".to_string(),
|
||||||
runtime_json: "{\"status\":\"running\"}".to_string(),
|
runtime_json: "{\"status\":\"running\"}".to_string(),
|
||||||
@@ -97,8 +97,8 @@ fn libsql_local_store_covers_control_plane_core_flows() {
|
|||||||
document_id: Some("doc_1".to_string()),
|
document_id: Some("doc_1".to_string()),
|
||||||
session_id: "sess_1".to_string(),
|
session_id: "sess_1".to_string(),
|
||||||
run_id: "run_1".to_string(),
|
run_id: "run_1".to_string(),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
event_type: "message.delta".to_string(),
|
event_type: "message.delta".to_string(),
|
||||||
payload_json: "{\"text\":\"hello\"}".to_string(),
|
payload_json: "{\"text\":\"hello\"}".to_string(),
|
||||||
})
|
})
|
||||||
@@ -181,8 +181,8 @@ fn libsql_local_store_handles_parallel_control_plane_writes() {
|
|||||||
session_id: "parallel_session".to_string(),
|
session_id: "parallel_session".to_string(),
|
||||||
run_id: "parallel_run".to_string(),
|
run_id: "parallel_run".to_string(),
|
||||||
title: Some("Parallel run".to_string()),
|
title: Some("Parallel run".to_string()),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
trace_id: None,
|
trace_id: None,
|
||||||
status: "running".to_string(),
|
status: "running".to_string(),
|
||||||
runtime_json: "{}".to_string(),
|
runtime_json: "{}".to_string(),
|
||||||
@@ -231,8 +231,8 @@ fn libsql_local_store_handles_parallel_control_plane_writes() {
|
|||||||
document_id: Some("parallel_doc".to_string()),
|
document_id: Some("parallel_doc".to_string()),
|
||||||
session_id: "parallel_session".to_string(),
|
session_id: "parallel_session".to_string(),
|
||||||
run_id: "parallel_run".to_string(),
|
run_id: "parallel_run".to_string(),
|
||||||
profile: "reasonix".to_string(),
|
profile: "pi".to_string(),
|
||||||
acp_runtime: "reasonix".to_string(),
|
acp_runtime: "pi".to_string(),
|
||||||
event_type: "message.delta".to_string(),
|
event_type: "message.delta".to_string(),
|
||||||
payload_json: format!("{{\"index\":{index}}}"),
|
payload_json: format!("{{\"index\":{index}}}"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ pub struct AiSession {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum AiRuntimeOwner {
|
pub enum AiRuntimeOwner {
|
||||||
RustWebHermes,
|
/// mnote-web 上的 Pi Lab / agent-tools 运行时(历史名 RustWebHermes 已退役)。
|
||||||
|
RustWebPi,
|
||||||
CompatReactIsland,
|
CompatReactIsland,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ pub struct DeleteBlock {
|
|||||||
/// 菜单项、快捷键、按钮的 enablement 通过 `when` 表达式对 context 求值得到。
|
/// 菜单项、快捷键、按钮的 enablement 通过 `when` 表达式对 context 求值得到。
|
||||||
///
|
///
|
||||||
/// 第一阶段支持 key 列表:
|
/// 第一阶段支持 key 列表:
|
||||||
/// - `workspace.sourceKind` — 工作区类型 ("local_folder" | "convex" | ...)
|
/// - `workspace.sourceKind` — 工作区类型 ("local_folder")
|
||||||
/// - `workspace.readonly` — bool,工作区是否只读
|
/// - `workspace.readonly` — bool,工作区是否只读
|
||||||
/// - `tree.focusKind` — 当前聚焦的树类型 ("file_tree" | "page_tree" | "none")
|
/// - `tree.focusKind` — 当前聚焦的树类型 ("file_tree" | "page_tree" | "none")
|
||||||
/// - `tree.selectionCount` — 当前选中行数 (i64)
|
/// - `tree.selectionCount` — 当前选中行数 (i64)
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ pub enum KernelProjectionKind {
|
|||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum WorkspaceSourceKind {
|
pub enum WorkspaceSourceKind {
|
||||||
LocalFolder,
|
LocalFolder,
|
||||||
ConvexWorkspace,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
@@ -897,8 +896,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn workspace_source_serializes_minimal_command_source_contract() {
|
fn workspace_source_serializes_minimal_command_source_contract() {
|
||||||
let source = WorkspaceSource {
|
let source = WorkspaceSource {
|
||||||
source_kind: WorkspaceSourceKind::ConvexWorkspace,
|
source_kind: WorkspaceSourceKind::LocalFolder,
|
||||||
root_uri: "convex://workspace/ws_1".into(),
|
root_uri: "file:///tmp/ws_1".into(),
|
||||||
workspace_id: "ws_1".into(),
|
workspace_id: "ws_1".into(),
|
||||||
capabilities: vec![
|
capabilities: vec![
|
||||||
WorkspaceSourceCapability::LoadSnapshot,
|
WorkspaceSourceCapability::LoadSnapshot,
|
||||||
@@ -908,8 +907,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let value = serde_json::to_value(&source).expect("workspace source 应可序列化");
|
let value = serde_json::to_value(&source).expect("workspace source 应可序列化");
|
||||||
assert_eq!(value["sourceKind"], json!("convex_workspace"));
|
assert_eq!(value["sourceKind"], json!("local_folder"));
|
||||||
assert_eq!(value["rootUri"], json!("convex://workspace/ws_1"));
|
assert_eq!(value["rootUri"], json!("file:///tmp/ws_1"));
|
||||||
assert_eq!(value["workspaceId"], json!("ws_1"));
|
assert_eq!(value["workspaceId"], json!("ws_1"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value["capabilities"],
|
value["capabilities"],
|
||||||
@@ -1026,8 +1025,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let value = serde_json::json!({
|
let value = serde_json::json!({
|
||||||
"workspaceId": "ws_1",
|
"workspaceId": "ws_1",
|
||||||
"sourceKind": "convex_workspace",
|
"sourceKind": "local_folder",
|
||||||
"rootUri": "convex://ws",
|
"rootUri": "file:///tmp/ws",
|
||||||
"relativePath": "doc.md",
|
"relativePath": "doc.md",
|
||||||
"objectIdentity": {
|
"objectIdentity": {
|
||||||
"objectKind": "page",
|
"objectKind": "page",
|
||||||
|
|||||||
@@ -589,7 +589,7 @@ pub fn plan_page_title(
|
|||||||
) -> CliResult<CliJsonOutput> {
|
) -> CliResult<CliJsonOutput> {
|
||||||
let bridge = build_bridge_context(ctx, workspace_id, "page", "title", page_id);
|
let bridge = build_bridge_context(ctx, workspace_id, "page", "title", page_id);
|
||||||
let command = CommandEnvelope {
|
let command = CommandEnvelope {
|
||||||
name: "documents.title.update".into(),
|
name: "tree.node.rename".into(),
|
||||||
command_id: build_command_id("page", "title", page_id),
|
command_id: build_command_id("page", "title", page_id),
|
||||||
idempotency_key: bridge.idempotency_key.clone(),
|
idempotency_key: bridge.idempotency_key.clone(),
|
||||||
actor: build_actor_payload(ctx),
|
actor: build_actor_payload(ctx),
|
||||||
@@ -644,7 +644,7 @@ pub fn plan_page_save(
|
|||||||
let content_value = parse_json_value(content_json, "content_json")?;
|
let content_value = parse_json_value(content_json, "content_json")?;
|
||||||
let bridge = build_bridge_context(ctx, workspace_id, "page", "save", page_id);
|
let bridge = build_bridge_context(ctx, workspace_id, "page", "save", page_id);
|
||||||
let command = CommandEnvelope {
|
let command = CommandEnvelope {
|
||||||
name: "documents.save".into(),
|
name: "page.body.save".into(),
|
||||||
command_id: build_command_id("page", "save", page_id),
|
command_id: build_command_id("page", "save", page_id),
|
||||||
idempotency_key: bridge.idempotency_key.clone(),
|
idempotency_key: bridge.idempotency_key.clone(),
|
||||||
actor: build_actor_payload(ctx),
|
actor: build_actor_payload(ctx),
|
||||||
@@ -699,7 +699,7 @@ pub fn plan_page_create(ctx: &CliContext, args: &PageCreateArgs<'_>) -> CliResul
|
|||||||
let content_value = parse_json_value(args.content_json, "content_json")?;
|
let content_value = parse_json_value(args.content_json, "content_json")?;
|
||||||
let bridge = build_bridge_context(ctx, Some(args.workspace_id), "page", "create", args.page_id);
|
let bridge = build_bridge_context(ctx, Some(args.workspace_id), "page", "create", args.page_id);
|
||||||
let command = CommandEnvelope {
|
let command = CommandEnvelope {
|
||||||
name: "documents.create".into(),
|
name: "tree.node.create".into(),
|
||||||
command_id: build_command_id("page", "create", args.page_id),
|
command_id: build_command_id("page", "create", args.page_id),
|
||||||
idempotency_key: bridge.idempotency_key.clone(),
|
idempotency_key: bridge.idempotency_key.clone(),
|
||||||
actor: build_actor_payload(ctx),
|
actor: build_actor_payload(ctx),
|
||||||
@@ -755,7 +755,7 @@ pub fn plan_page_create(ctx: &CliContext, args: &PageCreateArgs<'_>) -> CliResul
|
|||||||
pub fn plan_page_move(ctx: &CliContext, args: &PageMoveArgs<'_>) -> CliResult<CliJsonOutput> {
|
pub fn plan_page_move(ctx: &CliContext, args: &PageMoveArgs<'_>) -> CliResult<CliJsonOutput> {
|
||||||
let bridge = build_bridge_context(ctx, args.workspace_id, "page", "move", args.page_id);
|
let bridge = build_bridge_context(ctx, args.workspace_id, "page", "move", args.page_id);
|
||||||
let command = CommandEnvelope {
|
let command = CommandEnvelope {
|
||||||
name: "documents.move".into(),
|
name: "tree.subtree.move".into(),
|
||||||
command_id: build_command_id("page", "move", args.page_id),
|
command_id: build_command_id("page", "move", args.page_id),
|
||||||
idempotency_key: bridge.idempotency_key.clone(),
|
idempotency_key: bridge.idempotency_key.clone(),
|
||||||
actor: build_actor_payload(ctx),
|
actor: build_actor_payload(ctx),
|
||||||
@@ -805,7 +805,7 @@ pub fn plan_page_move(ctx: &CliContext, args: &PageMoveArgs<'_>) -> CliResult<Cl
|
|||||||
pub fn plan_page_delete(ctx: &CliContext, args: &PageDeleteArgs<'_>) -> CliResult<CliJsonOutput> {
|
pub fn plan_page_delete(ctx: &CliContext, args: &PageDeleteArgs<'_>) -> CliResult<CliJsonOutput> {
|
||||||
let bridge = build_bridge_context(ctx, args.workspace_id, "page", "delete", args.page_id);
|
let bridge = build_bridge_context(ctx, args.workspace_id, "page", "delete", args.page_id);
|
||||||
let command = CommandEnvelope {
|
let command = CommandEnvelope {
|
||||||
name: "documents.delete".into(),
|
name: "tree.node.archive".into(),
|
||||||
command_id: build_command_id("page", "delete", args.page_id),
|
command_id: build_command_id("page", "delete", args.page_id),
|
||||||
idempotency_key: bridge.idempotency_key.clone(),
|
idempotency_key: bridge.idempotency_key.clone(),
|
||||||
actor: build_actor_payload(ctx),
|
actor: build_actor_payload(ctx),
|
||||||
@@ -849,7 +849,7 @@ pub fn plan_page_delete(ctx: &CliContext, args: &PageDeleteArgs<'_>) -> CliResul
|
|||||||
pub fn plan_page_restore(ctx: &CliContext, args: &PageRestoreArgs<'_>) -> CliResult<CliJsonOutput> {
|
pub fn plan_page_restore(ctx: &CliContext, args: &PageRestoreArgs<'_>) -> CliResult<CliJsonOutput> {
|
||||||
let bridge = build_bridge_context(ctx, args.workspace_id, "page", "restore", args.page_id);
|
let bridge = build_bridge_context(ctx, args.workspace_id, "page", "restore", args.page_id);
|
||||||
let command = CommandEnvelope {
|
let command = CommandEnvelope {
|
||||||
name: "documents.restore".into(),
|
name: "tree.node.restore".into(),
|
||||||
command_id: build_command_id("page", "restore", args.page_id),
|
command_id: build_command_id("page", "restore", args.page_id),
|
||||||
idempotency_key: bridge.idempotency_key.clone(),
|
idempotency_key: bridge.idempotency_key.clone(),
|
||||||
actor: build_actor_payload(ctx),
|
actor: build_actor_payload(ctx),
|
||||||
@@ -1551,7 +1551,7 @@ fn execute_query(
|
|||||||
"documents.content.get"
|
"documents.content.get"
|
||||||
| "documents.meta.get"
|
| "documents.meta.get"
|
||||||
| "mindmaps.get"
|
| "mindmaps.get"
|
||||||
| "sidebar.dataset.list" => execute_retired_cloud_transport(name),
|
| "sidebar.dataset.list" => reject_non_local_cli_transport(name),
|
||||||
"search.documents" => execute_search_documents(transport, context, cli_ctx),
|
"search.documents" => execute_search_documents(transport, context, cli_ctx),
|
||||||
other => Err(CliError::validation(format!("暂不支持执行 query: {other}"))),
|
other => Err(CliError::validation(format!("暂不支持执行 query: {other}"))),
|
||||||
}
|
}
|
||||||
@@ -1565,13 +1565,13 @@ fn execute_command(
|
|||||||
cli_ctx: &CliContext,
|
cli_ctx: &CliContext,
|
||||||
) -> CliResult<Value> {
|
) -> CliResult<Value> {
|
||||||
match name {
|
match name {
|
||||||
"documents.create"
|
"tree.node.create"
|
||||||
| "documents.move"
|
| "tree.subtree.move"
|
||||||
| "documents.delete"
|
| "tree.node.archive"
|
||||||
| "documents.restore"
|
| "tree.node.restore"
|
||||||
| "documents.title.update"
|
| "tree.node.rename"
|
||||||
| "documents.save"
|
| "page.body.save"
|
||||||
| "mindmaps.put" => execute_retired_cloud_transport(name),
|
| "mindmaps.put" => reject_non_local_cli_transport(name),
|
||||||
"insert_block" => execute_block_insert(normalized_input, context, cli_ctx),
|
"insert_block" => execute_block_insert(normalized_input, context, cli_ctx),
|
||||||
"blocks.patch" => execute_block_patch(normalized_input, transport, cli_ctx),
|
"blocks.patch" => execute_block_patch(normalized_input, transport, cli_ctx),
|
||||||
"blocks.move" => execute_block_move(normalized_input, cli_ctx),
|
"blocks.move" => execute_block_move(normalized_input, cli_ctx),
|
||||||
@@ -1650,7 +1650,7 @@ fn execute_search_documents(
|
|||||||
_context: &CliOutputContext,
|
_context: &CliOutputContext,
|
||||||
_cli_ctx: &CliContext,
|
_cli_ctx: &CliContext,
|
||||||
) -> CliResult<Value> {
|
) -> CliResult<Value> {
|
||||||
execute_retired_cloud_transport("search.documents")
|
reject_non_local_cli_transport("search.documents")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_block_insert(
|
fn execute_block_insert(
|
||||||
@@ -1658,7 +1658,7 @@ fn execute_block_insert(
|
|||||||
_context: &CliOutputContext,
|
_context: &CliOutputContext,
|
||||||
_cli_ctx: &CliContext,
|
_cli_ctx: &CliContext,
|
||||||
) -> CliResult<Value> {
|
) -> CliResult<Value> {
|
||||||
execute_retired_cloud_transport("insert_block")
|
reject_non_local_cli_transport("insert_block")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_block_patch(
|
fn execute_block_patch(
|
||||||
@@ -1666,15 +1666,15 @@ fn execute_block_patch(
|
|||||||
_transport: &CliTransportPlan,
|
_transport: &CliTransportPlan,
|
||||||
_cli_ctx: &CliContext,
|
_cli_ctx: &CliContext,
|
||||||
) -> CliResult<Value> {
|
) -> CliResult<Value> {
|
||||||
execute_retired_cloud_transport("blocks.patch")
|
reject_non_local_cli_transport("blocks.patch")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_block_move(_normalized_input: &Value, _cli_ctx: &CliContext) -> CliResult<Value> {
|
fn execute_block_move(_normalized_input: &Value, _cli_ctx: &CliContext) -> CliResult<Value> {
|
||||||
execute_retired_cloud_transport("blocks.move")
|
reject_non_local_cli_transport("blocks.move")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_block_embed(_normalized_input: &Value, _cli_ctx: &CliContext) -> CliResult<Value> {
|
fn execute_block_embed(_normalized_input: &Value, _cli_ctx: &CliContext) -> CliResult<Value> {
|
||||||
execute_retired_cloud_transport("blocks.embed")
|
reject_non_local_cli_transport("blocks.embed")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_tool_target(
|
fn build_tool_target(
|
||||||
@@ -1735,7 +1735,7 @@ fn load_tool_data(
|
|||||||
"image_read" => Ok(json!({ "source": "cli", "asset": Value::Null })),
|
"image_read" => Ok(json!({ "source": "cli", "asset": Value::Null })),
|
||||||
"doc_get" | "doc_find" | "doc_insert_blocks" | "doc_replace_range" => {
|
"doc_get" | "doc_find" | "doc_insert_blocks" | "doc_replace_range" => {
|
||||||
let _ = read_doc_target(args, target)?;
|
let _ = read_doc_target(args, target)?;
|
||||||
Err(retired_cloud_transport_error(tool_name))
|
Err(non_local_cli_transport_error(tool_name))
|
||||||
}
|
}
|
||||||
"mindmap_get" | "mindmap_get_subtree" | "mindmap_apply_ops" | "mindmap_put" => {
|
"mindmap_get" | "mindmap_get_subtree" | "mindmap_apply_ops" | "mindmap_put" => {
|
||||||
let document_id = read_doc_target(args, target)?;
|
let document_id = read_doc_target(args, target)?;
|
||||||
@@ -1745,7 +1745,7 @@ fn load_tool_data(
|
|||||||
.or_else(|| target.and_then(|value| value.block_id.as_deref()))
|
.or_else(|| target.and_then(|value| value.block_id.as_deref()))
|
||||||
.ok_or_else(|| CliError::validation("mindmap 工具缺少 mindmapId"))?;
|
.ok_or_else(|| CliError::validation("mindmap 工具缺少 mindmapId"))?;
|
||||||
let _ = (document_id, mindmap_id);
|
let _ = (document_id, mindmap_id);
|
||||||
Err(retired_cloud_transport_error(tool_name))
|
Err(non_local_cli_transport_error(tool_name))
|
||||||
}
|
}
|
||||||
other => Err(CliError::validation(format!(
|
other => Err(CliError::validation(format!(
|
||||||
"暂不支持 tool 数据加载: {other}"
|
"暂不支持 tool 数据加载: {other}"
|
||||||
@@ -1753,13 +1753,13 @@ fn load_tool_data(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_retired_cloud_transport(operation: &str) -> CliResult<Value> {
|
fn reject_non_local_cli_transport(operation: &str) -> CliResult<Value> {
|
||||||
Err(retired_cloud_transport_error(operation))
|
Err(non_local_cli_transport_error(operation))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn retired_cloud_transport_error(operation: &str) -> CliError {
|
fn non_local_cli_transport_error(operation: &str) -> CliError {
|
||||||
CliError::validation(format!(
|
CliError::validation(format!(
|
||||||
"旧 Convex CLI 执行链已退役: {operation};请改用 local-first Rust/SQLite control-plane 路径"
|
"非 local_folder 的 CLI 执行链已移除: {operation};请使用 local-first local_folder 路径"
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2100,10 +2100,10 @@ mod tests {
|
|||||||
transport,
|
transport,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
assert_eq!(name, "documents.save");
|
assert_eq!(name, "page.body.save");
|
||||||
assert_eq!(command_id, "cmd_page_save_page_1");
|
assert_eq!(command_id, "cmd_page_save_page_1");
|
||||||
assert_eq!(transport.kind, "runtime_command_plan");
|
assert_eq!(transport.kind, "runtime_command_plan");
|
||||||
assert_eq!(transport.function_name, "documents.save");
|
assert_eq!(transport.function_name, "page.body.save");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
transport.args_json,
|
transport.args_json,
|
||||||
json!({
|
json!({
|
||||||
@@ -2149,9 +2149,9 @@ mod tests {
|
|||||||
CliOperationOutput::Command {
|
CliOperationOutput::Command {
|
||||||
name, transport, ..
|
name, transport, ..
|
||||||
} => {
|
} => {
|
||||||
assert_eq!(name, "documents.create");
|
assert_eq!(name, "tree.node.create");
|
||||||
assert_eq!(transport.kind, "runtime_command_plan");
|
assert_eq!(transport.kind, "runtime_command_plan");
|
||||||
assert_eq!(transport.function_name, "documents.create");
|
assert_eq!(transport.function_name, "tree.node.create");
|
||||||
assert_eq!(transport.args_json["workspaceId"], json!("ws_1"));
|
assert_eq!(transport.args_json["workspaceId"], json!("ws_1"));
|
||||||
assert_eq!(transport.args_json["parentId"], json!("parent_1"));
|
assert_eq!(transport.args_json["parentId"], json!("parent_1"));
|
||||||
}
|
}
|
||||||
@@ -2160,7 +2160,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn page_move_json_contract_uses_documents_move() {
|
fn page_move_json_contract_uses_tree_subtree_move() {
|
||||||
let output = plan_page_move(
|
let output = plan_page_move(
|
||||||
&CliContext::default(),
|
&CliContext::default(),
|
||||||
&PageMoveArgs {
|
&PageMoveArgs {
|
||||||
@@ -2176,9 +2176,9 @@ mod tests {
|
|||||||
CliOperationOutput::Command {
|
CliOperationOutput::Command {
|
||||||
name, transport, ..
|
name, transport, ..
|
||||||
} => {
|
} => {
|
||||||
assert_eq!(name, "documents.move");
|
assert_eq!(name, "tree.subtree.move");
|
||||||
assert_eq!(transport.kind, "runtime_command_plan");
|
assert_eq!(transport.kind, "runtime_command_plan");
|
||||||
assert_eq!(transport.function_name, "documents.move");
|
assert_eq!(transport.function_name, "tree.subtree.move");
|
||||||
assert_eq!(transport.args_json["sortOrder"], json!(3));
|
assert_eq!(transport.args_json["sortOrder"], json!(3));
|
||||||
}
|
}
|
||||||
_ => panic!("expected command output"),
|
_ => panic!("expected command output"),
|
||||||
|
|||||||
@@ -806,7 +806,7 @@ import {
|
|||||||
? urlFromCaller
|
? urlFromCaller
|
||||||
: new URL(`/documents/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
|
: new URL(`/documents/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
|
||||||
if (!url.searchParams.get('workspaceId') && descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
|
if (!url.searchParams.get('workspaceId') && descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
|
||||||
if (descriptor.sourceKind && descriptor.sourceKind !== 'convex_workspace') url.searchParams.set('sourceKind', descriptor.sourceKind);
|
if (descriptor.sourceKind) url.searchParams.set('sourceKind', descriptor.sourceKind);
|
||||||
if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
|
if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
|
||||||
pushUrlState(url);
|
pushUrlState(url);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ async function uploadLocalFolderAsset(file, plan, context) {
|
|||||||
|
|
||||||
async function uploadMediaAsset(file, plan, context) {
|
async function uploadMediaAsset(file, plan, context) {
|
||||||
document.documentElement.setAttribute('data-mnote-media-upload-retired', 'true');
|
document.documentElement.setAttribute('data-mnote-media-upload-retired', 'true');
|
||||||
throw new Error('旧 Convex Files 上传链已退役;local-first 附件请使用本地文件夹上传目标。');
|
throw new Error('仅支持本地文件夹上传。');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertUploadedAssetIntoEditor(asset, targetRoot, deps) {
|
async function insertUploadedAssetIntoEditor(asset, targetRoot, deps) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
|||||||
isFileTreePageRow,
|
isFileTreePageRow,
|
||||||
localFilePathFromAssetId,
|
localFilePathFromAssetId,
|
||||||
normalizeFileTreePageRenameTitle,
|
normalizeFileTreePageRenameTitle,
|
||||||
openConvexAssetFromFileTree,
|
openLocalAssetFromFileTree,
|
||||||
openEditorAttachmentDetail,
|
openEditorAttachmentDetail,
|
||||||
openEditorAttachmentDownload,
|
openEditorAttachmentDownload,
|
||||||
openEditorAttachmentEditTab,
|
openEditorAttachmentEditTab,
|
||||||
@@ -592,19 +592,19 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
|||||||
}
|
}
|
||||||
if (isAsset && action === 'new-window') {
|
if (isAsset && action === 'new-window') {
|
||||||
recordFileTreeAction('new-window', detail);
|
recordFileTreeAction('new-window', detail);
|
||||||
void openConvexAssetFromFileTree({ ...detail, openTarget: 'new-window' });
|
void openLocalAssetFromFileTree({ ...detail, openTarget: 'new-window' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isAsset && action === 'open-edit-mode') {
|
if (isAsset && action === 'open-edit-mode') {
|
||||||
withOfficeEditModeGuard(async function() {
|
withOfficeEditModeGuard(async function() {
|
||||||
recordFileTreeAction('open-edit-mode', detail);
|
recordFileTreeAction('open-edit-mode', detail);
|
||||||
void openConvexAssetFromFileTree({ ...detail, openTarget: 'edit-mode' });
|
void openLocalAssetFromFileTree({ ...detail, openTarget: 'edit-mode' });
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isAsset && action === 'open-right') {
|
if (isAsset && action === 'open-right') {
|
||||||
recordFileTreeAction('open-right', detail);
|
recordFileTreeAction('open-right', detail);
|
||||||
void openConvexAssetFromFileTree({ ...detail, openTarget: 'side' });
|
void openLocalAssetFromFileTree({ ...detail, openTarget: 'side' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === 'open-right') {
|
if (action === 'open-right') {
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openConvexAssetFromFileTree(detail) {
|
async function openLocalAssetFromFileTree(detail) {
|
||||||
var assetId = String(detail && detail.assetId || '').trim();
|
var assetId = String(detail && detail.assetId || '').trim();
|
||||||
if (!assetId) return;
|
if (!assetId) return;
|
||||||
var openTarget = String(detail && detail.openTarget || '').trim().toLowerCase();
|
var openTarget = String(detail && detail.openTarget || '').trim().toLowerCase();
|
||||||
@@ -520,11 +520,11 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
document.documentElement.setAttribute('data-mnote-media-sign-retired', 'true');
|
document.documentElement.setAttribute('data-mnote-media-sign-retired', 'true');
|
||||||
await window.mnote.alert('旧 Convex Files 附件签名链已退役;local-first 附件请通过本地文件夹资源打开。');
|
await window.mnote.alert('仅支持本地文件夹附件;请通过 local_folder 资源打开。');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('tree.asset.open', function(event) {
|
window.addEventListener('tree.asset.open', function(event) {
|
||||||
void openConvexAssetFromFileTree(event.detail || {});
|
void openLocalAssetFromFileTree(event.detail || {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -541,7 +541,7 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
|
|||||||
isMindmapAssetDetail,
|
isMindmapAssetDetail,
|
||||||
localFilePathFromAssetId,
|
localFilePathFromAssetId,
|
||||||
navigateToMindmapObject,
|
navigateToMindmapObject,
|
||||||
openConvexAssetFromFileTree,
|
openLocalAssetFromFileTree,
|
||||||
openLocalOfficeFileInActiveTab,
|
openLocalOfficeFileInActiveTab,
|
||||||
openLocalResourceInActiveTab,
|
openLocalResourceInActiveTab,
|
||||||
readFileTreeObjectIdentity,
|
readFileTreeObjectIdentity,
|
||||||
|
|||||||
@@ -1073,7 +1073,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
|||||||
const openLocalResourceInActiveTab = (...args) => sidebarFileTreeOpen.openLocalResourceInActiveTab(...args);
|
const openLocalResourceInActiveTab = (...args) => sidebarFileTreeOpen.openLocalResourceInActiveTab(...args);
|
||||||
const readFileTreeObjectIdentity = (...args) => sidebarFileTreeOpen.readFileTreeObjectIdentity(...args);
|
const readFileTreeObjectIdentity = (...args) => sidebarFileTreeOpen.readFileTreeObjectIdentity(...args);
|
||||||
const fetchCurrentOnlyOfficeUserId = (...args) => sidebarFileTreeOpen.fetchCurrentOnlyOfficeUserId(...args);
|
const fetchCurrentOnlyOfficeUserId = (...args) => sidebarFileTreeOpen.fetchCurrentOnlyOfficeUserId(...args);
|
||||||
const openConvexAssetFromFileTree = (...args) => sidebarFileTreeOpen.openConvexAssetFromFileTree(...args);
|
const openLocalAssetFromFileTree = (...args) => sidebarFileTreeOpen.openLocalAssetFromFileTree(...args);
|
||||||
|
|
||||||
var sidebarFileTreeUpload = null;
|
var sidebarFileTreeUpload = null;
|
||||||
const fileTreeRowsForUploadPreflight = (...args) => sidebarFileTreeUpload.fileTreeRowsForUploadPreflight(...args);
|
const fileTreeRowsForUploadPreflight = (...args) => sidebarFileTreeUpload.fileTreeRowsForUploadPreflight(...args);
|
||||||
@@ -2071,7 +2071,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.setAttribute('data-mnote-media-upload-retired', 'true');
|
document.documentElement.setAttribute('data-mnote-media-upload-retired', 'true');
|
||||||
throw new Error('旧 Convex Files 上传链已退役;local-first 附件请使用本地文件夹上传目标。');
|
throw new Error('仅支持本地文件夹上传。');
|
||||||
}
|
}
|
||||||
appendUploadedAssetRow(payload.asset, plan.targetDocumentId);
|
appendUploadedAssetRow(payload.asset, plan.targetDocumentId);
|
||||||
if (options && options.insertIntoEditor) {
|
if (options && options.insertIntoEditor) {
|
||||||
@@ -2219,7 +2219,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
|||||||
isFileTreePageRow,
|
isFileTreePageRow,
|
||||||
localFilePathFromAssetId,
|
localFilePathFromAssetId,
|
||||||
normalizeFileTreePageRenameTitle,
|
normalizeFileTreePageRenameTitle,
|
||||||
openConvexAssetFromFileTree,
|
openLocalAssetFromFileTree,
|
||||||
openEditorAttachmentDetail: (...args) => openEditorAttachmentDetail(...args),
|
openEditorAttachmentDetail: (...args) => openEditorAttachmentDetail(...args),
|
||||||
openEditorAttachmentDownload: (...args) => openEditorAttachmentDownload(...args),
|
openEditorAttachmentDownload: (...args) => openEditorAttachmentDownload(...args),
|
||||||
openEditorAttachmentEditTab: (...args) => openEditorAttachmentEditTab(...args),
|
openEditorAttachmentEditTab: (...args) => openEditorAttachmentEditTab(...args),
|
||||||
|
|||||||
@@ -223,8 +223,6 @@
|
|||||||
|
|
||||||
// Prefer WebSocket transport when available
|
// Prefer WebSocket transport when available
|
||||||
var requestedTransport = bootstrap.transport || 'tree-live-sse';
|
var requestedTransport = bootstrap.transport || 'tree-live-sse';
|
||||||
if (requestedTransport === 'convex-command-log-sse') requestedTransport = 'tree-live-sse';
|
|
||||||
if (requestedTransport === 'convex-command-log-ws') requestedTransport = 'tree-live-ws';
|
|
||||||
|
|
||||||
var preferWs = requestedTransport === 'tree-live-ws' && 'WebSocket' in window;
|
var preferWs = requestedTransport === 'tree-live-ws' && 'WebSocket' in window;
|
||||||
if (preferWs) {
|
if (preferWs) {
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export function buildTreeShellFileTreeContextMenuProfile(context, target) {
|
|||||||
const hasDocument = Boolean(getFileTreeRowDocumentId(item));
|
const hasDocument = Boolean(getFileTreeRowDocumentId(item));
|
||||||
const hasAsset = Boolean(getFileTreeRowAssetId(item));
|
const hasAsset = Boolean(getFileTreeRowAssetId(item));
|
||||||
const localSource = context.sourceKind === "local_folder";
|
const localSource = context.sourceKind === "local_folder";
|
||||||
const convexSource = context.sourceKind === "convex_workspace";
|
|
||||||
const canCreateFolder = localSource;
|
const canCreateFolder = localSource;
|
||||||
const canCreatePage = rowKind === "root" || rowKind === "folder" || rowKind === "document";
|
const canCreatePage = rowKind === "root" || rowKind === "folder" || rowKind === "document";
|
||||||
const canRename =
|
const canRename =
|
||||||
@@ -63,11 +62,11 @@ export function buildTreeShellFileTreeContextMenuProfile(context, target) {
|
|||||||
}),
|
}),
|
||||||
createFileTreeMenuItem("newFolder", "新建文件夹", {
|
createFileTreeMenuItem("newFolder", "新建文件夹", {
|
||||||
disabled: !canCreateFolder,
|
disabled: !canCreateFolder,
|
||||||
reason: convexSource ? "legacy cloud workspace 当前未暴露 folder capability" : "当前 source 不支持新建文件夹",
|
reason: "仅 local_folder 支持新建文件夹",
|
||||||
}),
|
}),
|
||||||
createFileTreeMenuItem("upload", "上传/导入", {
|
createFileTreeMenuItem("upload", "上传/导入", {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
reason: localSource ? "外部文件请拖入 Explorer" : "legacy cloud 上传 executor 尚未接入",
|
reason: localSource ? "外部文件请拖入 Explorer" : "仅 local_folder 支持上传",
|
||||||
}),
|
}),
|
||||||
createFileTreeMenuItem("refresh", "刷新", { separatorBefore: true }),
|
createFileTreeMenuItem("refresh", "刷新", { separatorBefore: true }),
|
||||||
createFileTreeMenuItem("collapseAll", "全部折叠"),
|
createFileTreeMenuItem("collapseAll", "全部折叠"),
|
||||||
@@ -95,7 +94,7 @@ export function buildTreeShellFileTreeContextMenuProfile(context, target) {
|
|||||||
}));
|
}));
|
||||||
items.push(createFileTreeMenuItem("newFolder", "新建文件夹", {
|
items.push(createFileTreeMenuItem("newFolder", "新建文件夹", {
|
||||||
disabled: !canCreateFolder,
|
disabled: !canCreateFolder,
|
||||||
reason: convexSource ? "legacy cloud workspace 当前未暴露 folder capability" : "当前 source 不支持新建文件夹",
|
reason: "仅 local_folder 支持新建文件夹",
|
||||||
}));
|
}));
|
||||||
items.push(createFileTreeMenuItem("paste", "粘贴", {
|
items.push(createFileTreeMenuItem("paste", "粘贴", {
|
||||||
disabled: !canPaste,
|
disabled: !canPaste,
|
||||||
|
|||||||
@@ -765,21 +765,7 @@ function startTreeShellRuntime() {
|
|||||||
scheduleRefresh();
|
scheduleRefresh();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (sourceKind === "convex_workspace") {
|
// non-local sources removed; only local_folder path above
|
||||||
for (const item of deletableItems) {
|
|
||||||
const documentId = getFileTreeRowDocumentId(item);
|
|
||||||
await sendCommand({
|
|
||||||
action: "delete",
|
|
||||||
workspaceId,
|
|
||||||
documentId,
|
|
||||||
});
|
|
||||||
applyRemovedDocumentLocally(documentId);
|
|
||||||
}
|
|
||||||
if (!deletableItems.every((item) => !fileTreeRowById.has(item.rowId))) {
|
|
||||||
scheduleRefresh();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
postToHost("tree.filetree.delete", {
|
postToHost("tree.filetree.delete", {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
rowIds: deletableItems.map((item) => item.rowId),
|
rowIds: deletableItems.map((item) => item.rowId),
|
||||||
@@ -823,21 +809,8 @@ function startTreeShellRuntime() {
|
|||||||
copy: fileTreeClipboard.action === "copy",
|
copy: fileTreeClipboard.action === "copy",
|
||||||
});
|
});
|
||||||
if (!accepted) return;
|
if (!accepted) return;
|
||||||
if (sourceKind === "convex_workspace") {
|
if (false) {
|
||||||
const sourceRowIds = filterRedundantFileTreeRowIds(fileTreeClipboard.rowIds);
|
// paste: local_folder only
|
||||||
for (const rowId of sourceRowIds) {
|
|
||||||
const sourceItem = fileTreeRowById.get(rowId);
|
|
||||||
const documentId = getFileTreeRowDocumentId(sourceItem);
|
|
||||||
if (!documentId) continue;
|
|
||||||
await sendCommand({
|
|
||||||
action: fileTreeClipboard.action === "copy" ? "copy" : "move",
|
|
||||||
workspaceId,
|
|
||||||
documentId,
|
|
||||||
parentId: target.documentId || null,
|
|
||||||
targetParentId: target.documentId || null,
|
|
||||||
sortOrder: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (fileTreeClipboard.action === "cut") {
|
if (fileTreeClipboard.action === "cut") {
|
||||||
fileTreeClipboard = { action: null, rowIds: [] };
|
fileTreeClipboard = { action: null, rowIds: [] };
|
||||||
}
|
}
|
||||||
@@ -1944,7 +1917,7 @@ function startTreeShellRuntime() {
|
|||||||
target: { documentId },
|
target: { documentId },
|
||||||
payload: { documentId },
|
payload: { documentId },
|
||||||
});
|
});
|
||||||
if (sourceKind === "convex_workspace" && applyCreatedDocumentLocally(result, parentId, title)) {
|
if (sourceKind === "local_folder" && applyCreatedDocumentLocally(result, parentId, title)) {
|
||||||
if (mode === "filetree") {
|
if (mode === "filetree") {
|
||||||
beginInlineRename("filetree", `doc:${documentId}`);
|
beginInlineRename("filetree", `doc:${documentId}`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tower_http::compression::CompressionLayer;
|
use tower_http::compression::CompressionLayer;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::{error, warn};
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
@@ -27,17 +27,11 @@ pub struct AppConfig {
|
|||||||
pub service_version: String,
|
pub service_version: String,
|
||||||
pub bind_addr: String,
|
pub bind_addr: String,
|
||||||
pub public_bind_addr: String,
|
pub public_bind_addr: String,
|
||||||
pub legacy_next_base_url: Option<String>,
|
|
||||||
pub enable_legacy_next_compat: bool,
|
|
||||||
pub enable_debug_shell_routes: bool,
|
pub enable_debug_shell_routes: bool,
|
||||||
pub enable_editor_actor: bool,
|
pub enable_editor_actor: bool,
|
||||||
pub enable_page_ai_pi_lab: bool,
|
pub enable_page_ai_pi_lab: bool,
|
||||||
pub compat_next_base_path: String,
|
|
||||||
pub convex_url: Option<String>,
|
|
||||||
pub convex_admin_key: Option<String>,
|
|
||||||
pub allow_dev_fixtures: bool,
|
pub allow_dev_fixtures: bool,
|
||||||
pub query_fixtures_json: Option<String>,
|
|
||||||
pub mutation_fixtures_json: Option<String>,
|
|
||||||
pub dev_user_id: String,
|
pub dev_user_id: String,
|
||||||
pub dev_user_name: String,
|
pub dev_user_name: String,
|
||||||
pub dev_user_email: String,
|
pub dev_user_email: String,
|
||||||
@@ -56,33 +50,17 @@ impl AppConfig {
|
|||||||
.unwrap_or_else(|_| "127.0.0.1:0".into()),
|
.unwrap_or_else(|_| "127.0.0.1:0".into()),
|
||||||
public_bind_addr: env::var("MNOTE_WEB_PUBLIC_BIND")
|
public_bind_addr: env::var("MNOTE_WEB_PUBLIC_BIND")
|
||||||
.unwrap_or_else(|_| "127.0.0.1:3000".into()),
|
.unwrap_or_else(|_| "127.0.0.1:3000".into()),
|
||||||
legacy_next_base_url: env::var("MNOTE_WEB_LEGACY_NEXT_BASE_URL")
|
|
||||||
.ok()
|
|
||||||
.map(|value| value.trim().trim_end_matches('/').to_string())
|
|
||||||
.filter(|value| !value.is_empty()),
|
|
||||||
enable_legacy_next_compat: env_bool("MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT", false),
|
|
||||||
enable_debug_shell_routes: env::var("MNOTE_WEB_ENABLE_DEBUG_SHELL_ROUTES")
|
enable_debug_shell_routes: env::var("MNOTE_WEB_ENABLE_DEBUG_SHELL_ROUTES")
|
||||||
.ok()
|
.ok()
|
||||||
.map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
.map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
enable_editor_actor: env_bool("MNOTE_WEB_ENABLE_EDITOR_ACTOR", true),
|
enable_editor_actor: env_bool("MNOTE_WEB_ENABLE_EDITOR_ACTOR", true),
|
||||||
enable_page_ai_pi_lab: env_bool("MNOTE_PAGE_AI_PI_LAB", true),
|
enable_page_ai_pi_lab: env_bool("MNOTE_PAGE_AI_PI_LAB", true),
|
||||||
compat_next_base_path: env::var("MNOTE_WEB_COMPAT_NEXT_BASE_PATH")
|
|
||||||
.unwrap_or_else(|_| "/api/compat/next".into()),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: env::var("MNOTE_WEB_ALLOW_DEV_FIXTURES")
|
allow_dev_fixtures: env::var("MNOTE_WEB_ALLOW_DEV_FIXTURES")
|
||||||
.ok()
|
.ok()
|
||||||
.map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
.map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
query_fixtures_json: env::var("MNOTE_WEB_QUERY_FIXTURES_JSON")
|
|
||||||
.ok()
|
|
||||||
.map(|value| value.trim().to_string())
|
|
||||||
.filter(|value| !value.is_empty()),
|
|
||||||
mutation_fixtures_json: env::var("MNOTE_WEB_MUTATION_FIXTURES_JSON")
|
|
||||||
.ok()
|
|
||||||
.map(|value| value.trim().to_string())
|
|
||||||
.filter(|value| !value.is_empty()),
|
|
||||||
dev_user_id: env::var("DEV_USER_ID")
|
dev_user_id: env::var("DEV_USER_ID")
|
||||||
.ok()
|
.ok()
|
||||||
.or_else(|| read_env_or_dotenv("DEV_USER_ID"))
|
.or_else(|| read_env_or_dotenv("DEV_USER_ID"))
|
||||||
@@ -367,23 +345,13 @@ async fn log_failed_response(request: Request, next: Next) -> Response {
|
|||||||
.get("x-error-code")
|
.get("x-error-code")
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
if error_code == "convex_retired" {
|
error!(
|
||||||
warn!(
|
method = %method,
|
||||||
method = %method,
|
uri = %uri,
|
||||||
uri = %uri,
|
status = %status,
|
||||||
status = %status,
|
error_code = %error_code,
|
||||||
error_code = %error_code,
|
"mnote-web 请求返回服务端错误"
|
||||||
"退役 legacy cloud/Convex 兼容路径被请求"
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
error!(
|
|
||||||
method = %method,
|
|
||||||
uri = %uri,
|
|
||||||
status = %status,
|
|
||||||
error_code = %error_code,
|
|
||||||
"mnote-web 请求返回服务端错误"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
@@ -399,17 +367,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: None,
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: true,
|
enable_page_ai_pi_lab: true,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: false,
|
allow_dev_fixtures: false,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! 职责:
|
//! 职责:
|
||||||
//! - 持有 per-document `EditorBlockDocument` 内存态
|
//! - 持有 per-document `EditorBlockDocument` 内存态
|
||||||
//! - 接收 EditorCommand,在内存中 apply,产生 diff
|
//! - 接收 EditorCommand,在内存中 apply,产生 diff
|
||||||
//! - Convex 持久化仍由 block.rs 通过 execute_page_body_save 完成
|
//! - 正文持久化由 block.rs 通过 execute_page_body_save 完成(local-first)
|
||||||
//!
|
//!
|
||||||
//! 不是 agent runtime:不维护会话、不做意图解析、不调模型(遵从 7-12 禁止项)。
|
//! 不是 agent runtime:不维护会话、不做意图解析、不调模型(遵从 7-12 禁止项)。
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@ impl EditorRuntimeActor {
|
|||||||
Ok((content, delta))
|
Ok((content, delta))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从内存态生成 legacy content(用于构建 Convex save payload)。
|
/// 从内存态生成 legacy content(用于构建 save payload)。
|
||||||
pub fn legacy_content_for_save(&self, document_id: &str) -> Result<Value, WebError> {
|
pub fn legacy_content_for_save(&self, document_id: &str) -> Result<Value, WebError> {
|
||||||
let documents = self
|
let documents = self
|
||||||
.documents
|
.documents
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::routes::api_access_token::{bearer_mnpat1, verify_pat_token};
|
use crate::routes::api_access_token::{bearer_mnpat1, verify_pat_token};
|
||||||
|
use crate::routes::vault::{bearer_mnv1, verify_agent_vault_token};
|
||||||
use crate::routes::vault_extension_token::{bearer_mnext1, verify_extension_token};
|
use crate::routes::vault_extension_token::{bearer_mnext1, verify_extension_token};
|
||||||
use axum::extract::Request;
|
use axum::extract::Request;
|
||||||
use axum::middleware::Next;
|
use axum::middleware::Next;
|
||||||
@@ -10,6 +11,7 @@ pub async fn inject_request_context(mut request: Request, next: Next) -> Respons
|
|||||||
RequestContext::from_http_parts(request.method(), request.uri(), request.headers());
|
RequestContext::from_http_parts(request.method(), request.uri(), request.headers());
|
||||||
|
|
||||||
// 7-76:Bearer mnpat1.* 优先于 cookie(PAT 请求忽略 cookie 并权)。
|
// 7-76:Bearer mnpat1.* 优先于 cookie(PAT 请求忽略 cookie 并权)。
|
||||||
|
// 注意:PAT 不能访问密码箱数据面(vault.rs require_authenticated 会拒绝)。
|
||||||
if let Some(token) = bearer_mnpat1(context.auth.authorization.as_deref()) {
|
if let Some(token) = bearer_mnpat1(context.auth.authorization.as_deref()) {
|
||||||
if let Ok(verified) = verify_pat_token(token) {
|
if let Ok(verified) = verify_pat_token(token) {
|
||||||
if let Some(actor) = crate::context::stable_actor_id(&verified.subject_user_id) {
|
if let Some(actor) = crate::context::stable_actor_id(&verified.subject_user_id) {
|
||||||
@@ -29,9 +31,29 @@ pub async fn inject_request_context(mut request: Request, next: Next) -> Respons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12-3 E2: Authorization Bearer mnext1.* → actor(仅当尚未 PAT / 仍 anonymous)
|
// 7-76 / 12-2:Bearer mnv1.* → agent vault capability(读密 list/get/resolve…)
|
||||||
|
// 仅当尚未被 mnpat1 绑定(避免 PAT 伪装成 vault token 混权)。
|
||||||
if context.auth.auth_method != "pat"
|
if context.auth.auth_method != "pat"
|
||||||
&& (context.auth.actor_id.trim() == "anonymous" || context.auth.actor_id.trim().is_empty())
|
&& (context.auth.actor_id.trim() == "anonymous" || context.auth.actor_id.trim().is_empty())
|
||||||
|
{
|
||||||
|
if let Some(token) = bearer_mnv1(context.auth.authorization.as_deref()) {
|
||||||
|
if let Ok(claims) = verify_agent_vault_token(token) {
|
||||||
|
if let Some(actor) = crate::context::stable_actor_id(&claims.actor) {
|
||||||
|
context.auth.actor_id = actor;
|
||||||
|
context.auth.actor_type = "ai_service".into();
|
||||||
|
context.auth.auth_method = "vault_token".into();
|
||||||
|
context.auth.scopes = claims.scope.clone();
|
||||||
|
context.auth.session_id = Some(format!("mnv1:{}", claims.jti));
|
||||||
|
context.auth.cookie_header = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12-3 E2: Authorization Bearer mnext1.* → actor(仅当尚未 PAT / vault / 仍 anonymous)
|
||||||
|
if context.auth.auth_method != "pat"
|
||||||
|
&& context.auth.auth_method != "vault_token"
|
||||||
|
&& (context.auth.actor_id.trim() == "anonymous" || context.auth.actor_id.trim().is_empty())
|
||||||
{
|
{
|
||||||
if let Some(token) = bearer_mnext1(context.auth.authorization.as_deref()) {
|
if let Some(token) = bearer_mnext1(context.auth.authorization.as_deref()) {
|
||||||
if let Ok(claims) = verify_extension_token(token) {
|
if let Ok(claims) = verify_extension_token(token) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::app::AppState;
|
|||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::mnote_agent_tools::ToolCallInput;
|
use crate::mnote_agent_tools::ToolCallInput;
|
||||||
use crate::routes::command_support::execute_runtime_command_via_legacy_cloud_with_artifacts;
|
use crate::routes::command_support::reject_non_local_runtime_command_with_artifacts;
|
||||||
use crate::routes::ensure_local_workspace_access;
|
use crate::routes::ensure_local_workspace_access;
|
||||||
use bridge_runtime::{
|
use bridge_runtime::{
|
||||||
RuntimeActorWire, RuntimeCommandEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire,
|
RuntimeActorWire, RuntimeCommandEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire,
|
||||||
@@ -230,7 +230,7 @@ async fn create_artifact_node(
|
|||||||
dry_run: false,
|
dry_run: false,
|
||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
context,
|
context,
|
||||||
workspace_id.as_deref(),
|
workspace_id.as_deref(),
|
||||||
@@ -247,9 +247,9 @@ async fn create_artifact_node(
|
|||||||
"to": artifact_document_id,
|
"to": artifact_document_id,
|
||||||
"kind": "ai_artifact_reference"
|
"kind": "ai_artifact_reference"
|
||||||
},
|
},
|
||||||
"result": execution.result,
|
"result": execution,
|
||||||
"artifacts": execution.artifacts,
|
"artifacts": None::<serde_json::Value>,
|
||||||
"artifactError": execution.artifact_error
|
"artifactError": None::<String>
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::mnote_agent_tools::doc::{
|
|||||||
required_arg,
|
required_arg,
|
||||||
};
|
};
|
||||||
use crate::mnote_agent_tools::ToolCallInput;
|
use crate::mnote_agent_tools::ToolCallInput;
|
||||||
use crate::routes::command_support::execute_runtime_command_via_legacy_cloud_with_artifacts;
|
use crate::routes::command_support::reject_non_local_runtime_command_with_artifacts;
|
||||||
use bridge_runtime::{
|
use bridge_runtime::{
|
||||||
apply_editor_command_to_legacy_content, RuntimeActorWire, RuntimeCommandEnvelopeWire,
|
apply_editor_command_to_legacy_content, RuntimeActorWire, RuntimeCommandEnvelopeWire,
|
||||||
RuntimeSourceWire, RuntimeTargetWire,
|
RuntimeSourceWire, RuntimeTargetWire,
|
||||||
@@ -1155,7 +1155,7 @@ async fn execute_page_body_save(
|
|||||||
dry_run: false,
|
dry_run: false,
|
||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
context,
|
context,
|
||||||
workspace_id.as_deref(),
|
workspace_id.as_deref(),
|
||||||
@@ -1166,9 +1166,9 @@ async fn execute_page_body_save(
|
|||||||
"commandName": "page.body.save",
|
"commandName": "page.body.save",
|
||||||
"commandId": command_id,
|
"commandId": command_id,
|
||||||
"changedBlocks": changed_blocks,
|
"changedBlocks": changed_blocks,
|
||||||
"result": execution.result,
|
"result": execution,
|
||||||
"artifacts": execution.artifacts,
|
"artifacts": None::<serde_json::Value>,
|
||||||
"artifactError": execution.artifact_error
|
"artifactError": None::<String>
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1229,7 +1229,7 @@ pub(crate) async fn execute_page_body_save_from_aggregate(
|
|||||||
dry_run: false,
|
dry_run: false,
|
||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
context,
|
context,
|
||||||
workspace_id.as_deref(),
|
workspace_id.as_deref(),
|
||||||
@@ -1240,9 +1240,9 @@ pub(crate) async fn execute_page_body_save_from_aggregate(
|
|||||||
"commandName": "page.body.save",
|
"commandName": "page.body.save",
|
||||||
"commandId": command_id,
|
"commandId": command_id,
|
||||||
"changedBlocks": changed_blocks,
|
"changedBlocks": changed_blocks,
|
||||||
"result": execution.result,
|
"result": execution,
|
||||||
"artifacts": execution.artifacts,
|
"artifacts": None::<serde_json::Value>,
|
||||||
"artifactError": execution.artifact_error
|
"artifactError": None::<String>
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ pub async fn doc_fetch(
|
|||||||
let workspace_id = input.effective_workspace_id();
|
let workspace_id = input.effective_workspace_id();
|
||||||
ensure_ai_scope_resource_allowed(context, input, &document_id)?;
|
ensure_ai_scope_resource_allowed(context, input, &document_id)?;
|
||||||
|
|
||||||
// 本地文件路径检测:直接读取授权 root 内的 .md 文件,不经过 Convex。
|
// 本地文件路径检测:直接读取授权 root 内的 .md 文件。
|
||||||
let is_local_file = document_id.starts_with('/')
|
let is_local_file = document_id.starts_with('/')
|
||||||
|| document_id.starts_with("./")
|
|| document_id.starts_with("./")
|
||||||
|| document_id.starts_with("file://");
|
|| document_id.starts_with("file://");
|
||||||
@@ -253,7 +253,7 @@ pub async fn doc_fetch(
|
|||||||
{
|
{
|
||||||
"local_fs"
|
"local_fs"
|
||||||
} else {
|
} else {
|
||||||
"convex"
|
"local_folder"
|
||||||
};
|
};
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"ok": true,
|
"ok": true,
|
||||||
@@ -1637,11 +1637,7 @@ pub async fn doc_markdown_edit(
|
|||||||
let blocks = block_projection_blocks(&aggregate);
|
let blocks = block_projection_blocks(&aggregate);
|
||||||
(
|
(
|
||||||
blocks_to_markdown(&blocks, true),
|
blocks_to_markdown(&blocks, true),
|
||||||
if is_local_workspace {
|
"local_folder",
|
||||||
"local_folder"
|
|
||||||
} else {
|
|
||||||
"convex"
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1749,7 +1745,7 @@ pub async fn doc_markdown_edit(
|
|||||||
String::from("无操作已应用")
|
String::from("无操作已应用")
|
||||||
};
|
};
|
||||||
|
|
||||||
// 5. 写回(本地文件直接 fs::write,Convex 文档通过 block ops apply)
|
// 5. 写回(本地文件直接 fs::write;否则走 local page body / block ops)
|
||||||
let apply_result = if let Some(ref path) = local_file_path {
|
let apply_result = if let Some(ref path) = local_file_path {
|
||||||
if input.dry_run.unwrap_or(false) {
|
if input.dry_run.unwrap_or(false) {
|
||||||
json!({"written": false, "dryRun": true, "path": path.display().to_string()})
|
json!({"written": false, "dryRun": true, "path": path.display().to_string()})
|
||||||
@@ -1900,7 +1896,7 @@ pub async fn doc_markdown_edit(
|
|||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
let execution =
|
let execution =
|
||||||
crate::routes::command_support::execute_runtime_command_via_legacy_cloud_with_artifacts(
|
crate::routes::command_support::reject_non_local_runtime_command_with_artifacts(
|
||||||
state,
|
state,
|
||||||
context,
|
context,
|
||||||
workspace_id.as_deref(),
|
workspace_id.as_deref(),
|
||||||
@@ -1911,9 +1907,9 @@ pub async fn doc_markdown_edit(
|
|||||||
"commandName": "page.body.save",
|
"commandName": "page.body.save",
|
||||||
"commandId": command_id,
|
"commandId": command_id,
|
||||||
"changedBlocks": changed_blocks,
|
"changedBlocks": changed_blocks,
|
||||||
"result": execution.result,
|
"result": execution,
|
||||||
"artifacts": execution.artifacts,
|
"artifacts": None::<serde_json::Value>,
|
||||||
"artifactError": execution.artifact_error
|
"artifactError": None::<String>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::app::AppState;
|
|||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::mnote_agent_tools::ToolCallInput;
|
use crate::mnote_agent_tools::ToolCallInput;
|
||||||
use crate::routes::command_support::execute_runtime_command_via_legacy_cloud_with_artifacts;
|
use crate::routes::command_support::reject_non_local_runtime_command_with_artifacts;
|
||||||
use crate::routes::web_shell::build_page_aggregate_snapshot;
|
use crate::routes::web_shell::build_page_aggregate_snapshot;
|
||||||
use bridge_runtime::{
|
use bridge_runtime::{
|
||||||
RuntimeActorWire, RuntimeCommandEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire,
|
RuntimeActorWire, RuntimeCommandEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire,
|
||||||
@@ -306,7 +306,7 @@ async fn page_command(
|
|||||||
dry_run: false,
|
dry_run: false,
|
||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
context,
|
context,
|
||||||
workspace_id.as_deref(),
|
workspace_id.as_deref(),
|
||||||
@@ -316,9 +316,9 @@ async fn page_command(
|
|||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
"commandName": command_name,
|
"commandName": command_name,
|
||||||
"commandId": command_id,
|
"commandId": command_id,
|
||||||
"result": execution.result,
|
"result": execution,
|
||||||
"artifacts": execution.artifacts,
|
"artifacts": None::<serde_json::Value>,
|
||||||
"artifactError": execution.artifact_error
|
"artifactError": None::<String>
|
||||||
});
|
});
|
||||||
merge_result_extra(&mut result, result_extra);
|
merge_result_extra(&mut result, result_extra);
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::app::{AppConfig, AppState};
|
|||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::routes::query_support::{
|
use crate::routes::query_support::{
|
||||||
execute_runtime_query_via_legacy_cloud, resolve_effective_workspace_id,
|
reject_non_local_runtime_query, resolve_effective_workspace_id,
|
||||||
};
|
};
|
||||||
use axum::extract::{Extension, Query, State};
|
use axum::extract::{Extension, Query, State};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
@@ -89,7 +89,7 @@ async fn execute_bridge_query(
|
|||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
query: RuntimeQueryEnvelopeWire,
|
query: RuntimeQueryEnvelopeWire,
|
||||||
) -> Result<Value, WebError> {
|
) -> Result<Value, WebError> {
|
||||||
execute_runtime_query_via_legacy_cloud(config, context, Some(workspace_id), query).await
|
reject_non_local_runtime_query(config, context, Some(workspace_id), query).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_resolved_workspace_id(
|
fn require_resolved_workspace_id(
|
||||||
@@ -184,17 +184,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(r#"{"sidebar:datasetList":{"documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}]},"bridgeLogs:listWorkspaceOverview":{"workspace_id":"ws_demo","command_logs":[{"command_id":"cmd_1","request_id":"req_1","status":"applied","created_at":"2026-04-16T00:00:00Z"}],"domain_events":[{"command_id":"cmd_1","status":"published","created_at":"2026-04-16T00:00:00Z"}],"next_cursor":null,"has_more":false,"filters":{"command_status":null,"event_status":null,"target_page_id":null,"target_block_id":null,"aggregate_type":null,"aggregate_id":null},"generated_at":"2026-04-16T00:00:00Z"}}"#.into()),
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
use crate::app::{AppConfig, AppState};
|
use crate::app::{AppConfig, AppState};
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::transport::legacy_cloud_guard::{
|
use crate::transport::local_only::local_required;
|
||||||
execute_retired_command_plan, execute_retired_command_plan_with_artifacts,
|
|
||||||
RetiredCloudCommandExecution,
|
|
||||||
};
|
|
||||||
use bridge_runtime::{
|
use bridge_runtime::{
|
||||||
execute_runtime_input, RuntimeActorWire, RuntimeBridgeContextWire, RuntimeCommandEnvelopeWire,
|
execute_runtime_input, RuntimeActorWire, RuntimeBridgeContextWire, RuntimeCommandEnvelopeWire,
|
||||||
RuntimeCommandExecutionPlan, RuntimeExecutionPlan, RuntimeInput, RuntimeSourceWire,
|
RuntimeCommandExecutionPlan, RuntimeExecutionPlan, RuntimeInput, RuntimeSourceWire,
|
||||||
RuntimeTargetWire,
|
RuntimeTargetWire,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
pub fn runtime_context(
|
pub fn runtime_context(
|
||||||
@@ -64,57 +60,24 @@ pub fn build_runtime_command_plan(
|
|||||||
Ok(plan)
|
Ok(plan)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute_runtime_command_via_legacy_cloud(
|
/// 非 local_folder 的 runtime command 一律拒绝(无 cloud 执行链)。
|
||||||
config: &AppConfig,
|
pub async fn reject_non_local_runtime_command(
|
||||||
|
_config: &AppConfig,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
effective_workspace_id: Option<&str>,
|
_effective_workspace_id: Option<&str>,
|
||||||
command: RuntimeCommandEnvelopeWire,
|
_command: RuntimeCommandEnvelopeWire,
|
||||||
) -> Result<Value, WebError> {
|
) -> Result<Value, WebError> {
|
||||||
let plan = build_runtime_command_plan(context, effective_workspace_id, command)?;
|
Err(local_required(context, "command_local_required"))
|
||||||
execute_retired_command_plan(config, context, &plan).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute_runtime_command_via_legacy_cloud_with_artifacts(
|
/// 历史 with_artifacts 形态;永远 local_required,无产物/无 cloud 执行。
|
||||||
state: &AppState,
|
pub async fn reject_non_local_runtime_command_with_artifacts(
|
||||||
|
_state: &AppState,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
effective_workspace_id: Option<&str>,
|
_effective_workspace_id: Option<&str>,
|
||||||
command: RuntimeCommandEnvelopeWire,
|
_command: RuntimeCommandEnvelopeWire,
|
||||||
) -> Result<RetiredCloudCommandExecution, WebError> {
|
) -> Result<Value, WebError> {
|
||||||
let runtime_context = runtime_context(context, effective_workspace_id);
|
Err(local_required(context, "command_local_required"))
|
||||||
let runtime_input = RuntimeInput::Command {
|
|
||||||
context: runtime_context.clone(),
|
|
||||||
command: command.clone(),
|
|
||||||
};
|
|
||||||
let RuntimeExecutionPlan::Command(plan) = execute_runtime_input(runtime_input)
|
|
||||||
.map_err(|error| WebError::bad_request(error.message).with_context(context))?
|
|
||||||
else {
|
|
||||||
return Err(WebError::internal("runtime command 未返回 command plan").with_context(context));
|
|
||||||
};
|
|
||||||
|
|
||||||
let execution = execute_retired_command_plan_with_artifacts(
|
|
||||||
state.config(),
|
|
||||||
context,
|
|
||||||
&runtime_context,
|
|
||||||
&command,
|
|
||||||
&plan,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Push stream delta notification via broadcast for WebSocket/SSE push consumers
|
|
||||||
let workspace_id = effective_workspace_id
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.or_else(|| context.workspace.workspace_id.clone());
|
|
||||||
let delta = json!({
|
|
||||||
"kind": "command_committed",
|
|
||||||
"commandName": command.name,
|
|
||||||
"commandId": command.command_id,
|
|
||||||
"workspaceId": workspace_id,
|
|
||||||
"requestId": context.trace.request_id,
|
|
||||||
"traceId": context.trace.trace_id,
|
|
||||||
});
|
|
||||||
let _ = state.stream_delta_tx.send(delta);
|
|
||||||
|
|
||||||
Ok(execution)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_tree_target(
|
pub fn build_tree_target(
|
||||||
|
|||||||
@@ -59,32 +59,41 @@ mod tests {
|
|||||||
use crate::app::{build_app, AppConfig, AppState};
|
use crate::app::{build_app, AppConfig, AppState};
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::http::{Request, StatusCode};
|
use axum::http::{Request, StatusCode};
|
||||||
use axum::response::IntoResponse;
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
use tower::util::ServiceExt;
|
use tower::util::ServiceExt;
|
||||||
|
|
||||||
fn app() -> axum::Router {
|
fn test_config() -> AppConfig {
|
||||||
build_app(AppState::new(AppConfig {
|
AppConfig {
|
||||||
service_name: "mnote-web".into(),
|
service_name: "mnote-web".into(),
|
||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(r#"{"sidebar:datasetList":{"active_workspace_id":"ws_demo","workspaces":[],"documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"trashed_documents":[],"media_assets":[],"trashed_media_assets":[],"mindmap_assets":[],"trashed_mindmap_assets":[],"table_assets":[],"trashed_table_assets":[],"mindmap_docs":[],"mindmap_asset_children":{}},"bridgeLogs:listWorkspaceOverview":{"workspace_id":"ws_demo","command_logs":[],"domain_events":[],"next_cursor":null,"has_more":false,"filters":{"command_status":null,"event_status":null,"target_page_id":null,"target_block_id":null,"aggregate_type":null,"aggregate_id":null},"generated_at":"2026-04-16T00:00:00Z"}}"#.into()),
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
environment: "dev".into(),
|
environment: "dev".into(),
|
||||||
}))
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app() -> axum::Router {
|
||||||
|
build_app(AppState::new(test_config()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_ai_agent_run(body: &'static str) -> axum::http::Response<Body> {
|
||||||
|
build_app(AppState::new(test_config()))
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.method("POST")
|
||||||
|
.uri("/api/ai-agent/run")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(body))
|
||||||
|
.expect("request"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("response")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -104,37 +113,10 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn direct_ai_agent_run_returns_legacy_retired_guard() {
|
async fn direct_ai_agent_run_returns_legacy_retired_guard() {
|
||||||
let response = build_app(AppState::new(AppConfig {
|
let response = post_ai_agent_run(
|
||||||
service_name: "mnote-web".into(),
|
r#"{"stream":true,"scope":"document","messages":[{"role":"user","content":"ping"}],"context":{"documentId":"doc-1"},"options":{"ai":{"provider":"online"}}}"#,
|
||||||
service_version: "0.1.0".into(),
|
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes: false,
|
|
||||||
enable_editor_actor: true,
|
|
||||||
enable_page_ai_pi_lab: false,
|
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
|
||||||
dev_user_name: "开发用户".into(),
|
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
|
||||||
environment: "dev".into(),
|
|
||||||
}))
|
|
||||||
.oneshot(
|
|
||||||
Request::builder()
|
|
||||||
.method("POST")
|
|
||||||
.uri("/api/ai-agent/run")
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.body(Body::from(r#"{"stream":true,"scope":"document","messages":[{"role":"user","content":"ping"}],"context":{"documentId":"doc-1"},"options":{"ai":{"provider":"online"}}}"#))
|
|
||||||
.expect("request"),
|
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
.expect("response");
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::GONE);
|
assert_eq!(response.status(), StatusCode::GONE);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -161,37 +143,10 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn explicit_agent_provider_returns_legacy_retired_guard() {
|
async fn explicit_agent_provider_returns_legacy_retired_guard() {
|
||||||
let response = build_app(AppState::new(AppConfig {
|
let response = post_ai_agent_run(
|
||||||
service_name: "mnote-web".into(),
|
r#"{"stream":true,"scope":"document","messages":[{"role":"user","content":"ping"}],"context":{"documentId":"doc-1"},"options":{"ai":{"provider":"codex"}}}"#,
|
||||||
service_version: "0.1.0".into(),
|
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:9".into()),
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes: false,
|
|
||||||
enable_editor_actor: true,
|
|
||||||
enable_page_ai_pi_lab: false,
|
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
|
||||||
dev_user_name: "开发用户".into(),
|
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
|
||||||
environment: "dev".into(),
|
|
||||||
}))
|
|
||||||
.oneshot(
|
|
||||||
Request::builder()
|
|
||||||
.method("POST")
|
|
||||||
.uri("/api/ai-agent/run")
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.body(Body::from(r#"{"stream":true,"scope":"document","messages":[{"role":"user","content":"ping"}],"context":{"documentId":"doc-1"},"options":{"ai":{"provider":"codex"}}}"#))
|
|
||||||
.expect("request"),
|
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
.expect("response");
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::GONE);
|
assert_eq!(response.status(), StatusCode::GONE);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -209,58 +164,13 @@ mod tests {
|
|||||||
assert!(text.contains("legacy_ai_agent_run_retired"));
|
assert!(text.contains("legacy_ai_agent_run_retired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 退役 brand provider 必须 410,且不得静默落到任何旁路执行。
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn explicit_retired_hermes_provider_fails_instead_of_using_legacy_next_or_direct_bridge() {
|
async fn explicit_retired_hermes_provider_returns_gone() {
|
||||||
let next_app = axum::Router::new().route(
|
let response = post_ai_agent_run(
|
||||||
"/api/ai-agent/run",
|
r#"{"stream":true,"scope":"document","messages":[{"role":"user","content":"ping"}],"context":{"documentId":"doc-1"},"options":{"ai":{"provider":"hermes"}}}"#,
|
||||||
axum::routing::post(|| async move {
|
|
||||||
(
|
|
||||||
[(
|
|
||||||
axum::http::header::CONTENT_TYPE,
|
|
||||||
"text/event-stream; charset=utf-8",
|
|
||||||
)],
|
|
||||||
"event: assistant_message\ndata: {\"text\":\"hello from next ai\"}\n\n",
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind next");
|
|
||||||
let addr = listener.local_addr().expect("local addr");
|
|
||||||
let server = tokio::spawn(async move {
|
|
||||||
axum::serve(listener, next_app).await.expect("serve next");
|
|
||||||
});
|
|
||||||
|
|
||||||
let response = build_app(AppState::new(AppConfig {
|
|
||||||
service_name: "mnote-web".into(),
|
|
||||||
service_version: "0.1.0".into(),
|
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
|
||||||
legacy_next_base_url: Some(format!("http://{}", addr)),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
|
||||||
enable_editor_actor: true,
|
|
||||||
enable_page_ai_pi_lab: false,
|
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
|
||||||
dev_user_name: "开发用户".into(),
|
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
|
||||||
environment: "dev".into(),
|
|
||||||
}))
|
|
||||||
.oneshot(
|
|
||||||
Request::builder()
|
|
||||||
.method("POST")
|
|
||||||
.uri("/api/ai-agent/run")
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.body(Body::from(r#"{"stream":true,"scope":"document","messages":[{"role":"user","content":"ping"}],"context":{"documentId":"doc-1"},"options":{"ai":{"provider":"hermes"}}}"#))
|
|
||||||
.expect("request"),
|
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
.expect("response");
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::GONE);
|
assert_eq!(response.status(), StatusCode::GONE);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -275,8 +185,7 @@ mod tests {
|
|||||||
.expect("body");
|
.expect("body");
|
||||||
let text = String::from_utf8(body.to_vec()).expect("utf8");
|
let text = String::from_utf8(body.to_vec()).expect("utf8");
|
||||||
assert!(text.contains("hermes"));
|
assert!(text.contains("hermes"));
|
||||||
assert!(!text.contains("hello from next ai"));
|
assert!(text.contains("legacy_ai_agent_run_retired"));
|
||||||
|
assert!(text.contains("Pi Lab"));
|
||||||
server.abort();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -518,17 +518,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: None,
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: false,
|
enable_editor_actor: false,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures,
|
allow_dev_fixtures,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ use crate::context::RequestContext;
|
|||||||
use crate::document_buffer_store::{self, BufferKey, BufferStore};
|
use crate::document_buffer_store::{self, BufferKey, BufferStore};
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::routes::command_support::{
|
use crate::routes::command_support::{
|
||||||
execute_runtime_command_via_legacy_cloud,
|
reject_non_local_runtime_command,
|
||||||
execute_runtime_command_via_legacy_cloud_with_artifacts,
|
reject_non_local_runtime_command_with_artifacts,
|
||||||
};
|
};
|
||||||
use crate::routes::local_folder_source::{
|
use crate::routes::local_folder_source::{
|
||||||
ensure_local_workspace_access, update_local_markdown_title, write_local_markdown_page_body,
|
ensure_local_workspace_access, update_local_markdown_title, write_local_markdown_page_body,
|
||||||
};
|
};
|
||||||
use crate::routes::query_support::{
|
use crate::routes::query_support::{
|
||||||
execute_runtime_query_via_legacy_cloud, fetch_documents_meta_via_legacy_cloud,
|
reject_non_local_runtime_query, reject_non_local_documents_meta,
|
||||||
resolve_effective_workspace_id,
|
resolve_effective_workspace_id,
|
||||||
};
|
};
|
||||||
use axum::extract::{Extension, Query, State};
|
use axum::extract::{Extension, Query, State};
|
||||||
@@ -22,8 +22,6 @@ use bridge_runtime::{
|
|||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::fs;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -187,7 +185,6 @@ pub struct DocumentEmptyTrashRequest {
|
|||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
const NEXT_DOCUMENTS_BASE_URL_ENV: &str = "MNOTE_NEXT_BASE_URL";
|
|
||||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||||
const HEADER_DOCUMENTS_TRANSPORT: &str = "x-mnote-documents-transport";
|
const HEADER_DOCUMENTS_TRANSPORT: &str = "x-mnote-documents-transport";
|
||||||
|
|
||||||
@@ -207,14 +204,8 @@ fn ok_response(context: &RequestContext, result: Value) -> (StatusCode, HeaderMa
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execution_artifacts_json(
|
fn execution_artifacts_json(_execution: &Value) -> Value {
|
||||||
execution: &crate::transport::legacy_cloud_guard::RetiredCloudCommandExecution,
|
Value::Null
|
||||||
) -> Value {
|
|
||||||
execution
|
|
||||||
.artifacts
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|artifacts| serde_json::to_value(artifacts).ok())
|
|
||||||
.unwrap_or(Value::Null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stamp_documents_headers(headers: &mut HeaderMap) {
|
fn stamp_documents_headers(headers: &mut HeaderMap) {
|
||||||
@@ -226,312 +217,6 @@ fn stamp_documents_headers(headers: &mut HeaderMap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_env_or_dotenv(key: &str) -> Option<String> {
|
|
||||||
if let Ok(value) = std::env::var(key) {
|
|
||||||
let trimmed = value.trim().trim_matches('"').to_string();
|
|
||||||
if !trimmed.is_empty() {
|
|
||||||
return Some(trimmed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.join("../../..")
|
|
||||||
.join(".env.all");
|
|
||||||
let content = fs::read_to_string(root).ok()?;
|
|
||||||
for line in content.lines() {
|
|
||||||
let line = line.trim_end_matches('\r');
|
|
||||||
if line.starts_with('#') || line.trim().is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some((k, v)) = line.split_once('=') else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if k.trim() != key {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let trimmed = v.trim().trim_matches('"').to_string();
|
|
||||||
if !trimmed.is_empty() {
|
|
||||||
return Some(trimmed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_documents_base_url() -> String {
|
|
||||||
read_env_or_dotenv(NEXT_DOCUMENTS_BASE_URL_ENV)
|
|
||||||
.unwrap_or_else(|| "http://127.0.0.1:3000".into())
|
|
||||||
.trim()
|
|
||||||
.trim_end_matches('/')
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_proxy_via_next(_context: &RequestContext) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_next_proxy_headers(
|
|
||||||
context: &RequestContext,
|
|
||||||
effective_workspace_id: Option<&str>,
|
|
||||||
) -> reqwest::header::HeaderMap {
|
|
||||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
|
||||||
|
|
||||||
fn insert(headers: &mut HeaderMap, name: &'static str, value: &str) {
|
|
||||||
let Ok(header_name) = HeaderName::from_lowercase(name.as_bytes()) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Ok(header_value) = HeaderValue::from_str(value) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
headers.insert(header_name, header_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
if let Some(cookie) = context.auth.cookie_header.as_deref() {
|
|
||||||
insert(&mut headers, "cookie", cookie);
|
|
||||||
}
|
|
||||||
if let Some(authorization) = context.auth.authorization.as_deref() {
|
|
||||||
insert(&mut headers, "authorization", authorization);
|
|
||||||
}
|
|
||||||
insert(&mut headers, "x-request-id", &context.trace.request_id);
|
|
||||||
insert(&mut headers, "x-trace-id", &context.trace.trace_id);
|
|
||||||
insert(
|
|
||||||
&mut headers,
|
|
||||||
"x-mnote-source-channel",
|
|
||||||
&context.source.channel,
|
|
||||||
);
|
|
||||||
insert(
|
|
||||||
&mut headers,
|
|
||||||
"x-mnote-source-client",
|
|
||||||
&context.source.client,
|
|
||||||
);
|
|
||||||
insert(&mut headers, "x-mnote-actor-id", &context.auth.actor_id);
|
|
||||||
insert(&mut headers, "x-mnote-actor-type", &context.auth.actor_type);
|
|
||||||
if let Some(session_id) = context.auth.session_id.as_deref() {
|
|
||||||
insert(&mut headers, "x-mnote-session-id", session_id);
|
|
||||||
}
|
|
||||||
if let Some(workspace_id) = effective_workspace_id {
|
|
||||||
insert(&mut headers, "x-mnote-workspace-id", workspace_id);
|
|
||||||
}
|
|
||||||
headers
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_next_documents_request(
|
|
||||||
context: &RequestContext,
|
|
||||||
request: reqwest::RequestBuilder,
|
|
||||||
phase: &'static str,
|
|
||||||
) -> Result<Value, WebError> {
|
|
||||||
let response = request.send().await.map_err(|error| {
|
|
||||||
let base = if error.is_timeout() {
|
|
||||||
WebError::gateway_timeout_code(
|
|
||||||
"next_proxy_timeout",
|
|
||||||
format!("Next compat 请求超时: {error}"),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
WebError::service_unavailable_code(
|
|
||||||
"next_proxy_unavailable",
|
|
||||||
format!("Next compat 请求失败: {error}"),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
base.with_context(context)
|
|
||||||
.with_header("x-error-phase", phase)
|
|
||||||
.with_header("x-upstream-service", "next")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let status = response.status();
|
|
||||||
let text = response.text().await.map_err(|error| {
|
|
||||||
WebError::bad_gateway_code(
|
|
||||||
"next_proxy_bad_response",
|
|
||||||
format!("Next compat 响应读取失败: {error}"),
|
|
||||||
)
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", phase)
|
|
||||||
.with_header("x-upstream-service", "next")
|
|
||||||
.with_header("x-upstream-status", status.as_u16().to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let payload = serde_json::from_str::<Value>(&text).map_err(|_| {
|
|
||||||
let snippet: String = text.chars().take(180).collect();
|
|
||||||
WebError::bad_gateway_code(
|
|
||||||
"next_proxy_bad_response",
|
|
||||||
format!("Next compat 返回了非 JSON 内容: {snippet}"),
|
|
||||||
)
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", phase)
|
|
||||||
.with_header("x-upstream-service", "next")
|
|
||||||
.with_header("x-upstream-status", status.as_u16().to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !status.is_success() {
|
|
||||||
let message = payload
|
|
||||||
.get("error")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.or_else(|| payload.get("message").and_then(Value::as_str))
|
|
||||||
.unwrap_or("Next compat 文档接口请求失败");
|
|
||||||
let web_error = match status {
|
|
||||||
reqwest::StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN => {
|
|
||||||
WebError::new(StatusCode::UNAUTHORIZED, "next_proxy_unauthorized", message)
|
|
||||||
}
|
|
||||||
reqwest::StatusCode::NOT_FOUND => {
|
|
||||||
WebError::new(StatusCode::NOT_FOUND, "next_proxy_not_found", message)
|
|
||||||
}
|
|
||||||
_ => WebError::bad_gateway_code("next_proxy_error", message),
|
|
||||||
};
|
|
||||||
return Err(web_error
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", phase)
|
|
||||||
.with_header("x-upstream-service", "next")
|
|
||||||
.with_header("x-upstream-status", status.as_u16().to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn proxy_next_documents_meta(
|
|
||||||
context: &RequestContext,
|
|
||||||
effective_workspace_id: Option<&str>,
|
|
||||||
document_id: &str,
|
|
||||||
) -> Result<Value, WebError> {
|
|
||||||
let base_url = next_documents_base_url();
|
|
||||||
let mut url =
|
|
||||||
reqwest::Url::parse(&format!("{base_url}/api/documents/meta")).map_err(|error| {
|
|
||||||
WebError::internal(format!("Next compat meta URL 非法: {error}"))
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", "next_proxy_meta_url")
|
|
||||||
.with_header("x-upstream-service", "next")
|
|
||||||
})?;
|
|
||||||
url.query_pairs_mut().append_pair("documentId", document_id);
|
|
||||||
if let Some(workspace_id) = effective_workspace_id {
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("workspaceId", workspace_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(Duration::from_secs(20))
|
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
|
||||||
.build()
|
|
||||||
.map_err(|error| {
|
|
||||||
WebError::internal(format!("Next compat HTTP 客户端创建失败: {error}"))
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", "next_proxy_meta_client")
|
|
||||||
.with_header("x-upstream-service", "next")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let payload = send_next_documents_request(
|
|
||||||
context,
|
|
||||||
client
|
|
||||||
.get(url)
|
|
||||||
.headers(build_next_proxy_headers(context, effective_workspace_id)),
|
|
||||||
"next_proxy_meta",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(payload.get("doc").cloned().unwrap_or(Value::Null))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn proxy_next_documents_content(
|
|
||||||
context: &RequestContext,
|
|
||||||
effective_workspace_id: Option<&str>,
|
|
||||||
document_id: &str,
|
|
||||||
) -> Result<Value, WebError> {
|
|
||||||
let base_url = next_documents_base_url();
|
|
||||||
let mut url =
|
|
||||||
reqwest::Url::parse(&format!("{base_url}/api/documents/content")).map_err(|error| {
|
|
||||||
WebError::internal(format!("Next compat content URL 非法: {error}"))
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", "next_proxy_content_url")
|
|
||||||
.with_header("x-upstream-service", "next")
|
|
||||||
})?;
|
|
||||||
url.query_pairs_mut().append_pair("documentId", document_id);
|
|
||||||
if let Some(workspace_id) = effective_workspace_id {
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("workspaceId", workspace_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(Duration::from_secs(20))
|
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
|
||||||
.build()
|
|
||||||
.map_err(|error| {
|
|
||||||
WebError::internal(format!("Next compat HTTP 客户端创建失败: {error}"))
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", "next_proxy_content_client")
|
|
||||||
.with_header("x-upstream-service", "next")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let payload = send_next_documents_request(
|
|
||||||
context,
|
|
||||||
client
|
|
||||||
.get(url)
|
|
||||||
.headers(build_next_proxy_headers(context, effective_workspace_id)),
|
|
||||||
"next_proxy_content",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(json!({
|
|
||||||
"content": payload.get("content").cloned().unwrap_or(Value::Null),
|
|
||||||
"revision": payload.get("revision").cloned().unwrap_or(Value::Null),
|
|
||||||
"conflictDetectionKey": payload.get("conflictDetectionKey").cloned().unwrap_or(Value::Null),
|
|
||||||
"pageSubtree": payload.get("pageSubtree").cloned().unwrap_or(Value::Null),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn proxy_next_documents_save(
|
|
||||||
context: &RequestContext,
|
|
||||||
effective_workspace_id: Option<&str>,
|
|
||||||
body: &DocumentSaveRequest,
|
|
||||||
) -> Result<Value, WebError> {
|
|
||||||
let base_url = next_documents_base_url();
|
|
||||||
let url = reqwest::Url::parse(&format!("{base_url}/api/documents/save")).map_err(|error| {
|
|
||||||
WebError::internal(format!("Next compat save URL 非法: {error}"))
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", "next_proxy_save_url")
|
|
||||||
.with_header("x-upstream-service", "next")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(Duration::from_secs(20))
|
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
|
||||||
.build()
|
|
||||||
.map_err(|error| {
|
|
||||||
WebError::internal(format!("Next compat HTTP 客户端创建失败: {error}"))
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", "next_proxy_save_client")
|
|
||||||
.with_header("x-upstream-service", "next")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let payload = send_next_documents_request(
|
|
||||||
context,
|
|
||||||
client
|
|
||||||
.post(url)
|
|
||||||
.headers(build_next_proxy_headers(context, effective_workspace_id))
|
|
||||||
.json(&json!({
|
|
||||||
"documentId": body.document_id,
|
|
||||||
"workspaceId": effective_workspace_id,
|
|
||||||
"revision": body.revision,
|
|
||||||
"conflictDetectionKey": body.conflict_detection_key,
|
|
||||||
"expectedFileVersion": body.expected_file_version,
|
|
||||||
"writeIntentId": body.write_intent_id,
|
|
||||||
"saveOperationId": body.save_operation_id,
|
|
||||||
"editorDocument": body.editor_document,
|
|
||||||
"content": body.content,
|
|
||||||
"tiptapDocument": body.tiptap_document,
|
|
||||||
"snapshotCapturedAt": body.snapshot_captured_at,
|
|
||||||
"blockCount": body.block_count,
|
|
||||||
})),
|
|
||||||
"next_proxy_save",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(json!({
|
|
||||||
"revision": payload.get("revision").cloned().unwrap_or(Value::Null),
|
|
||||||
"conflictDetectionKey": payload
|
|
||||||
.get("conflictDetectionKey")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or(Value::Null),
|
|
||||||
"ok": payload.get("ok").cloned().unwrap_or(Value::Bool(true)),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn load_document_meta_result(
|
pub async fn load_document_meta_result(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
@@ -546,15 +231,7 @@ pub async fn load_document_meta_result(
|
|||||||
}
|
}
|
||||||
let effective_workspace_id =
|
let effective_workspace_id =
|
||||||
resolve_effective_workspace_id(context, query.workspace_id.as_deref(), false)?;
|
resolve_effective_workspace_id(context, query.workspace_id.as_deref(), false)?;
|
||||||
if should_proxy_via_next(context) {
|
reject_non_local_documents_meta(
|
||||||
return proxy_next_documents_meta(
|
|
||||||
context,
|
|
||||||
effective_workspace_id.as_deref(),
|
|
||||||
&document_id_owned,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
fetch_documents_meta_via_legacy_cloud(
|
|
||||||
state.config(),
|
state.config(),
|
||||||
context,
|
context,
|
||||||
effective_workspace_id.as_deref(),
|
effective_workspace_id.as_deref(),
|
||||||
@@ -577,15 +254,7 @@ pub async fn load_document_content_result(
|
|||||||
}
|
}
|
||||||
let effective_workspace_id =
|
let effective_workspace_id =
|
||||||
resolve_effective_workspace_id(context, query.workspace_id.as_deref(), false)?;
|
resolve_effective_workspace_id(context, query.workspace_id.as_deref(), false)?;
|
||||||
if should_proxy_via_next(context) {
|
reject_non_local_runtime_query(
|
||||||
return proxy_next_documents_content(
|
|
||||||
context,
|
|
||||||
effective_workspace_id.as_deref(),
|
|
||||||
&document_id_owned,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
execute_runtime_query_via_legacy_cloud(
|
|
||||||
state.config(),
|
state.config(),
|
||||||
context,
|
context,
|
||||||
effective_workspace_id.as_deref(),
|
effective_workspace_id.as_deref(),
|
||||||
@@ -789,11 +458,6 @@ pub async fn save(
|
|||||||
}
|
}
|
||||||
let effective_workspace_id =
|
let effective_workspace_id =
|
||||||
resolve_effective_workspace_id(&context, body.workspace_id.as_deref(), false)?;
|
resolve_effective_workspace_id(&context, body.workspace_id.as_deref(), false)?;
|
||||||
if should_proxy_via_next(&context) {
|
|
||||||
let result =
|
|
||||||
proxy_next_documents_save(&context, effective_workspace_id.as_deref(), &body).await?;
|
|
||||||
return Ok(ok_response(&context, result));
|
|
||||||
}
|
|
||||||
let command = RuntimeCommandEnvelopeWire {
|
let command = RuntimeCommandEnvelopeWire {
|
||||||
name: "page.body.save".into(),
|
name: "page.body.save".into(),
|
||||||
command_id: format!("document_save_{}", context.trace.request_id),
|
command_id: format!("document_save_{}", context.trace.request_id),
|
||||||
@@ -836,19 +500,19 @@ pub async fn save(
|
|||||||
dry_run: false,
|
dry_run: false,
|
||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
&context,
|
&context,
|
||||||
effective_workspace_id.as_deref(),
|
effective_workspace_id.as_deref(),
|
||||||
command,
|
command,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let mut result = execution.result;
|
let mut result = execution;
|
||||||
if let Value::Object(map) = &mut result {
|
if let Value::Object(map) = &mut result {
|
||||||
map.insert("executedCommand".into(), json!("page.body.save"));
|
map.insert("executedCommand".into(), json!("page.body.save"));
|
||||||
map.insert("canonicalCommand".into(), json!("page.body.save"));
|
map.insert("canonicalCommand".into(), json!("page.body.save"));
|
||||||
map.insert("compatRoute".into(), json!("/api/documents/save"));
|
map.insert("compatRoute".into(), json!("/api/documents/save"));
|
||||||
if let Some(artifact_error) = execution.artifact_error {
|
if let Some(artifact_error) = None::<String> {
|
||||||
map.insert("artifactError".into(), json!(artifact_error));
|
map.insert("artifactError".into(), json!(artifact_error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -868,7 +532,7 @@ pub async fn purge(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let command = RuntimeCommandEnvelopeWire {
|
let command = RuntimeCommandEnvelopeWire {
|
||||||
name: "documents.purge".into(),
|
name: "tree.node.purge".into(),
|
||||||
command_id: format!("document_purge_{}", context.trace.request_id),
|
command_id: format!("document_purge_{}", context.trace.request_id),
|
||||||
idempotency_key: context.source.idempotency_key.clone(),
|
idempotency_key: context.source.idempotency_key.clone(),
|
||||||
actor: RuntimeActorWire {
|
actor: RuntimeActorWire {
|
||||||
@@ -893,13 +557,13 @@ pub async fn purge(
|
|||||||
"documentId": document_id,
|
"documentId": document_id,
|
||||||
}),
|
}),
|
||||||
preflight_data: None,
|
preflight_data: None,
|
||||||
reason: Some("mnote-web documents purge compat".into()),
|
reason: Some("mnote-web tree node purge".into()),
|
||||||
refs: vec!["mnote-web-documents-compat".into()],
|
refs: vec!["mnote-web-tree-purge".into()],
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
let result =
|
let result =
|
||||||
execute_runtime_command_via_legacy_cloud(state.config(), &context, None, command).await?;
|
reject_non_local_runtime_command(state.config(), &context, None, command).await?;
|
||||||
Ok(ok_response(&context, result))
|
Ok(ok_response(&context, result))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,13 +607,13 @@ pub async fn empty_trash(
|
|||||||
preflight_data: None,
|
preflight_data: None,
|
||||||
reason: Some("mnote-web tree trash empty workspace".into()),
|
reason: Some("mnote-web tree trash empty workspace".into()),
|
||||||
refs: vec![
|
refs: vec![
|
||||||
"mnote-web-documents-trash-compat".into(),
|
"mnote-web-tree-trash".into(),
|
||||||
"tree.trash.emptyWorkspace".into(),
|
"tree.trash.emptyWorkspace".into(),
|
||||||
],
|
],
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
&context,
|
&context,
|
||||||
Some(workspace_id),
|
Some(workspace_id),
|
||||||
@@ -957,8 +621,8 @@ pub async fn empty_trash(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let artifacts = execution_artifacts_json(&execution);
|
let artifacts = execution_artifacts_json(&execution);
|
||||||
let artifact_error = execution.artifact_error.clone();
|
let artifact_error = None::<String>;
|
||||||
let mut result = execution.result;
|
let mut result = execution;
|
||||||
if let Value::Object(map) = &mut result {
|
if let Value::Object(map) = &mut result {
|
||||||
map.insert(
|
map.insert(
|
||||||
"canonicalCommand".into(),
|
"canonicalCommand".into(),
|
||||||
@@ -979,7 +643,6 @@ pub async fn empty_trash(
|
|||||||
"meta": {
|
"meta": {
|
||||||
"commandName": "tree.trash.emptyWorkspace",
|
"commandName": "tree.trash.emptyWorkspace",
|
||||||
"canonicalCommand": "tree.trash.emptyWorkspace",
|
"canonicalCommand": "tree.trash.emptyWorkspace",
|
||||||
"compatCommandName": "documents.emptyTrashByWorkspace",
|
|
||||||
"artifacts": artifacts,
|
"artifacts": artifacts,
|
||||||
"artifactError": artifact_error,
|
"artifactError": artifact_error,
|
||||||
},
|
},
|
||||||
@@ -1062,7 +725,7 @@ pub async fn title(
|
|||||||
dry_run: false,
|
dry_run: false,
|
||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
&context,
|
&context,
|
||||||
effective_workspace_id.as_deref(),
|
effective_workspace_id.as_deref(),
|
||||||
@@ -1070,8 +733,8 @@ pub async fn title(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let artifacts = execution_artifacts_json(&execution);
|
let artifacts = execution_artifacts_json(&execution);
|
||||||
let artifact_error = execution.artifact_error.clone();
|
let artifact_error = None::<String>;
|
||||||
let result = execution.result;
|
let result = execution;
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
stamp_documents_headers(&mut headers);
|
stamp_documents_headers(&mut headers);
|
||||||
Ok((
|
Ok((
|
||||||
@@ -1166,7 +829,7 @@ pub async fn options(
|
|||||||
dry_run: false,
|
dry_run: false,
|
||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
&context,
|
&context,
|
||||||
effective_workspace_id.as_deref(),
|
effective_workspace_id.as_deref(),
|
||||||
@@ -1174,8 +837,8 @@ pub async fn options(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let artifacts = execution_artifacts_json(&execution);
|
let artifacts = execution_artifacts_json(&execution);
|
||||||
let artifact_error = execution.artifact_error.clone();
|
let artifact_error = None::<String>;
|
||||||
let result = execution.result;
|
let result = execution;
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
stamp_documents_headers(&mut headers);
|
stamp_documents_headers(&mut headers);
|
||||||
Ok((
|
Ok((
|
||||||
@@ -1212,91 +875,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"documents:getMeta": {
|
|
||||||
"id": "doc_1",
|
|
||||||
"workspace_id": "ws_demo",
|
|
||||||
"title": "服务端页面",
|
|
||||||
"updated_at": "2026-04-18T09:30:00Z",
|
|
||||||
"can_edit": true,
|
|
||||||
"disable_download": false,
|
|
||||||
"disable_copy": false,
|
|
||||||
"wide_layout": false,
|
|
||||||
"use_small_text": false,
|
|
||||||
"show_heading_numbers": true,
|
|
||||||
"show_toc": true,
|
|
||||||
"show_structure": true,
|
|
||||||
"protect_editing": false,
|
|
||||||
"show_word_count": true,
|
|
||||||
"collapse_backlinks": false,
|
|
||||||
"page_font": "default",
|
|
||||||
"layout_density": "normal",
|
|
||||||
"hide_child_pages": false,
|
|
||||||
"show_block_ref_count": true,
|
|
||||||
"embed_default_block_id": "heading_1",
|
|
||||||
"word_count": 42,
|
|
||||||
"character_count": 128,
|
|
||||||
"block_count": 3,
|
|
||||||
"todo_total": 1,
|
|
||||||
"todo_done": 0
|
|
||||||
},
|
|
||||||
"documents:getContent": {
|
|
||||||
"title": "服务端页面",
|
|
||||||
"content": [
|
|
||||||
{
|
|
||||||
"id": "heading_1",
|
|
||||||
"type": "heading",
|
|
||||||
"props": { "level": 1 },
|
|
||||||
"content": [{ "type": "text", "text": "章节一" }]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"revision": 7,
|
|
||||||
"conflict_detection_key": "doc_1:7"
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
mutation_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"documents:updateTitle": {
|
|
||||||
"ok": true,
|
|
||||||
"title": "服务端页面(改名)"
|
|
||||||
},
|
|
||||||
"documents:updateOptions": {
|
|
||||||
"ok": true,
|
|
||||||
"show_toc": false
|
|
||||||
},
|
|
||||||
"documents:updateContent": {
|
|
||||||
"ok": true,
|
|
||||||
"updated_at": "2026-04-18T09:45:00Z",
|
|
||||||
"revision": 8,
|
|
||||||
"conflict_detection_key": "doc_1:8"
|
|
||||||
},
|
|
||||||
"documents:emptyTrashByWorkspace": {
|
|
||||||
"ok": true,
|
|
||||||
"deletedCount": 2
|
|
||||||
},
|
|
||||||
"bridgeLogs:recordCommandLog": {
|
|
||||||
"ok": true,
|
|
||||||
"id": "clog_fixture"
|
|
||||||
},
|
|
||||||
"bridgeLogs:recordDomainEvent": {
|
|
||||||
"ok": true,
|
|
||||||
"id": "evt_fixture"
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -1329,8 +912,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn convex_auth_cookie_does_not_force_next_proxy() {
|
fn stale_third_party_auth_cookie_does_not_change_request_identity_source() {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
|
// 残留第三方 cookie 名不得成为身份真源;真源仍是 mnote_session。
|
||||||
headers.insert(
|
headers.insert(
|
||||||
"cookie",
|
"cookie",
|
||||||
HeaderValue::from_static("__convexAuthJWT=jwt-demo; foo=bar"),
|
HeaderValue::from_static("__convexAuthJWT=jwt-demo; foo=bar"),
|
||||||
@@ -1341,7 +925,8 @@ mod tests {
|
|||||||
&headers,
|
&headers,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(!super::should_proxy_via_next(&context));
|
assert_ne!(context.auth.actor_id.as_str(), "jwt-demo");
|
||||||
|
assert!(context.auth.session_id.is_none() || context.auth.session_id.as_deref() != Some("jwt-demo"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1775,141 +1775,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: true,
|
enable_debug_shell_routes: true,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"documents:getMeta": {
|
|
||||||
"id": "doc_shell",
|
|
||||||
"workspace_id": "ws_demo",
|
|
||||||
"title": "编辑器壳页面",
|
|
||||||
"updated_at": "2026-04-18T11:22:33Z",
|
|
||||||
"can_edit": true,
|
|
||||||
"show_structure": true
|
|
||||||
},
|
|
||||||
"documents:getContent": {
|
|
||||||
"title": "编辑器壳页面",
|
|
||||||
"content": [
|
|
||||||
{
|
|
||||||
"id": "heading_1",
|
|
||||||
"type": "heading",
|
|
||||||
"props": { "level": 1 },
|
|
||||||
"content": [{ "type": "text", "text": "第一节" }]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"revision": 9,
|
|
||||||
"conflict_detection_key": "doc_shell:9",
|
|
||||||
"page_subtree": {
|
|
||||||
"projection_id": "kernel_projection:page_tree:doc_shell",
|
|
||||||
"projection": "page_tree",
|
|
||||||
"root_node_id": "doc_shell",
|
|
||||||
"root_node": {
|
|
||||||
"id": "doc_shell",
|
|
||||||
"parent_node_id": null,
|
|
||||||
"node_type": "page",
|
|
||||||
"block_id": null,
|
|
||||||
"anchor_block_id": null,
|
|
||||||
"depth": 0,
|
|
||||||
"metadata": {
|
|
||||||
"title": "编辑器壳页面",
|
|
||||||
"text_snippet": "页面首段",
|
|
||||||
"block_type": null,
|
|
||||||
"heading_level": null,
|
|
||||||
"numbering": null,
|
|
||||||
"child_count": 2,
|
|
||||||
"order": 0,
|
|
||||||
"path": ["编辑器壳页面"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"subtree": {
|
|
||||||
"root_node_id": "doc_shell",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "doc_shell",
|
|
||||||
"parent_node_id": null,
|
|
||||||
"node_type": "page",
|
|
||||||
"block_id": null,
|
|
||||||
"anchor_block_id": null,
|
|
||||||
"depth": 0,
|
|
||||||
"metadata": {
|
|
||||||
"title": "编辑器壳页面",
|
|
||||||
"text_snippet": "页面首段",
|
|
||||||
"block_type": null,
|
|
||||||
"heading_level": null,
|
|
||||||
"numbering": null,
|
|
||||||
"child_count": 2,
|
|
||||||
"order": 0,
|
|
||||||
"path": ["编辑器壳页面"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "node_heading_1",
|
|
||||||
"parent_node_id": "doc_shell",
|
|
||||||
"node_type": "section",
|
|
||||||
"block_id": "heading_1",
|
|
||||||
"anchor_block_id": "heading_1",
|
|
||||||
"depth": 1,
|
|
||||||
"metadata": {
|
|
||||||
"title": "第一节",
|
|
||||||
"text_snippet": "第一节 页面首段",
|
|
||||||
"block_type": "heading",
|
|
||||||
"heading_level": 1,
|
|
||||||
"numbering": "1",
|
|
||||||
"child_count": 1,
|
|
||||||
"order": 0,
|
|
||||||
"path": ["编辑器壳页面", "第一节"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "node_para_1",
|
|
||||||
"parent_node_id": "node_heading_1",
|
|
||||||
"node_type": "content_node",
|
|
||||||
"block_id": "paragraph_1",
|
|
||||||
"anchor_block_id": null,
|
|
||||||
"depth": 2,
|
|
||||||
"metadata": {
|
|
||||||
"title": null,
|
|
||||||
"text_snippet": "这是正文第一段",
|
|
||||||
"block_type": "paragraph",
|
|
||||||
"heading_level": null,
|
|
||||||
"numbering": null,
|
|
||||||
"child_count": 0,
|
|
||||||
"order": 1,
|
|
||||||
"path": ["编辑器壳页面", "第一节", "这是正文第一段"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"outline": [
|
|
||||||
{
|
|
||||||
"id": "outline_heading_1",
|
|
||||||
"node_id": "node_heading_1",
|
|
||||||
"anchor_block_id": "heading_1",
|
|
||||||
"title": "第一节",
|
|
||||||
"level": 1,
|
|
||||||
"numbering": "1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"evidence": [],
|
|
||||||
"stats": {
|
|
||||||
"block_count": 3,
|
|
||||||
"heading_count": 1,
|
|
||||||
"evidence_count": 0,
|
|
||||||
"max_depth": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -384,17 +384,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -294,17 +294,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(r#"{"sidebar:datasetList":{"documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"media_assets":[{"id":"asset_file_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"封面.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_child_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"节点图片.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_pdf_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"说明书.pdf","mime_type":"application/pdf","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_book_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"小说.epub","mime_type":"application/epub+zip","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_assets":[{"id":"mind_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"mindmap","file_name":"头脑风暴.json","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"table_assets":[{"id":"table_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"table","file_name":"预算.table","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_asset_children":{"mind_1":["asset_child_1"]}},"bridgeLogs:listWorkspaceOverview":{"workspace_id":"ws_demo","command_logs":[{"command_id":"cmd_1","request_id":"req_1","status":"applied","created_at":"2026-04-16T00:00:00Z"}],"domain_events":[{"command_id":"cmd_1","status":"published","created_at":"2026-04-16T00:00:00Z"}],"next_cursor":null,"has_more":false,"filters":{"command_status":null,"event_status":null,"target_page_id":null,"target_block_id":null,"aggregate_type":null,"aggregate_id":null},"generated_at":"2026-04-16T00:00:00Z"}}"#.into()),
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -392,7 +386,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn tree_projection_routes_short_circuit_local_folder_without_convex_dataset() {
|
async fn tree_projection_routes_short_circuit_local_folder_without_cloud_dataset() {
|
||||||
let root = std::env::temp_dir().join(format!(
|
let root = std::env::temp_dir().join(format!(
|
||||||
"mnote-local-kernel-projection-{}",
|
"mnote-local-kernel-projection-{}",
|
||||||
std::process::id()
|
std::process::id()
|
||||||
|
|||||||
@@ -437,10 +437,11 @@ pub fn pat_is_installed(config: &AppConfig, token_id: &str) -> bool {
|
|||||||
pub fn install_status_for_pat(config: &AppConfig, token_id: &str, jti: &str) -> Value {
|
pub fn install_status_for_pat(config: &AppConfig, token_id: &str, jti: &str) -> Value {
|
||||||
let env = environment_name(config);
|
let env = environment_name(config);
|
||||||
let base = public_base_url(config);
|
let base = public_base_url(config);
|
||||||
let state = load_install_state();
|
// 统一经 helper 判定,与列表 / install 一致,并消除 dead_code 警告
|
||||||
let entry = state.environments.get(&env);
|
let meta = current_pat_install_for_token(config, token_id, jti);
|
||||||
let meta = entry.and_then(|e| find_pat_meta_by_token(e, token_id, jti));
|
let installed = pat_is_installed(config, token_id)
|
||||||
let installed = meta.is_some();
|
|| (!jti.is_empty() && jti != token_id && pat_is_installed(config, jti))
|
||||||
|
|| meta.is_some();
|
||||||
let subject = meta
|
let subject = meta
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|m| m.subject_user_id.clone())
|
.map(|m| m.subject_user_id.clone())
|
||||||
@@ -450,7 +451,6 @@ pub fn install_status_for_pat(config: &AppConfig, token_id: &str, jti: &str) ->
|
|||||||
} else {
|
} else {
|
||||||
display_env_path(&user_agent_env_path(&subject, &env))
|
display_env_path(&user_agent_env_path(&subject, &env))
|
||||||
};
|
};
|
||||||
// 已无共享 active 槽;installed 即该用户本机已配置
|
|
||||||
json!({
|
json!({
|
||||||
"installed": installed,
|
"installed": installed,
|
||||||
"active": installed,
|
"active": installed,
|
||||||
@@ -1015,17 +1015,11 @@ mod tests {
|
|||||||
service_version: "test".into(),
|
service_version: "test".into(),
|
||||||
bind_addr: bind.into(),
|
bind_addr: bind.into(),
|
||||||
public_bind_addr: bind.into(),
|
public_bind_addr: bind.into(),
|
||||||
legacy_next_base_url: None,
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: false,
|
enable_editor_actor: false,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: false,
|
allow_dev_fixtures: false,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev".into(),
|
dev_user_id: "dev".into(),
|
||||||
dev_user_name: "dev".into(),
|
dev_user_name: "dev".into(),
|
||||||
dev_user_email: "dev@test".into(),
|
dev_user_email: "dev@test".into(),
|
||||||
|
|||||||
@@ -500,17 +500,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: false,
|
allow_dev_fixtures: false,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -11111,17 +11111,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: None,
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: false,
|
allow_dev_fixtures: false,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ fn document_for_target_row(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn upload(Extension(context): Extension<RequestContext>) -> Response {
|
pub async fn upload(Extension(context): Extension<RequestContext>) -> Response {
|
||||||
retired_convex_media_response(&context, "upload", None)
|
retired_media_response(&context, "upload", None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn filetree_upload_target_preflight(
|
pub async fn filetree_upload_target_preflight(
|
||||||
@@ -164,10 +164,10 @@ pub async fn sign(
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|value| !value.is_empty());
|
.filter(|value| !value.is_empty());
|
||||||
retired_convex_media_response(&context, "sign", asset_id)
|
retired_media_response(&context, "sign", asset_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn retired_convex_media_response(
|
fn retired_media_response(
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
operation: &str,
|
operation: &str,
|
||||||
asset_id: Option<&str>,
|
asset_id: Option<&str>,
|
||||||
@@ -176,9 +176,9 @@ fn retired_convex_media_response(
|
|||||||
StatusCode::GONE,
|
StatusCode::GONE,
|
||||||
Json(json!({
|
Json(json!({
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"code": "mnote_media_convex_retired",
|
"code": "mnote_media_local_required",
|
||||||
"error": "旧 Convex Files media route 已退役",
|
"error": "media 仅支持 local-folder 附件路径",
|
||||||
"message": "旧 /api/media Convex Files 上传与签名链已退役;local-first 附件请使用 /api/local-folder/assets/upload 与 /api/local-folder/files/open。",
|
"message": "/api/media 云上传链已移除;请使用 /api/local-folder/assets/upload 与 /api/local-folder/files/open。",
|
||||||
"operation": operation,
|
"operation": operation,
|
||||||
"assetId": asset_id,
|
"assetId": asset_id,
|
||||||
"replacement": {
|
"replacement": {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use crate::app::AppState;
|
use crate::app::AppState;
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::routes::command_support::execute_runtime_command_via_legacy_cloud_with_artifacts;
|
use crate::routes::command_support::reject_non_local_runtime_command_with_artifacts;
|
||||||
use crate::routes::local_folder_source::{
|
use crate::routes::local_folder_source::{
|
||||||
ensure_local_workspace_read_access_with_state, ensure_local_workspace_write_access_with_state,
|
ensure_local_workspace_read_access_with_state, ensure_local_workspace_write_access_with_state,
|
||||||
read_local_mindmap_data, write_local_mindmap_data,
|
read_local_mindmap_data, write_local_mindmap_data,
|
||||||
};
|
};
|
||||||
use crate::routes::query_support::{
|
use crate::routes::query_support::{
|
||||||
execute_runtime_query_against_data, execute_runtime_query_via_legacy_cloud,
|
execute_runtime_query_against_data, reject_non_local_runtime_query,
|
||||||
fetch_documents_meta_via_legacy_cloud, fetch_query_data_via_legacy_cloud,
|
reject_non_local_documents_meta, reject_non_local_query_data,
|
||||||
resolve_effective_workspace_id,
|
resolve_effective_workspace_id,
|
||||||
};
|
};
|
||||||
use axum::extract::{Extension, Path, Query, State};
|
use axum::extract::{Extension, Path, Query, State};
|
||||||
@@ -124,18 +124,12 @@ async fn resolve_mindmap_workspace_id(
|
|||||||
// 思维导图 runtime 的历史请求体不一定带 workspaceId;
|
// 思维导图 runtime 的历史请求体不一定带 workspaceId;
|
||||||
// 这里从页面 meta 反查,确保后续 command artifacts 能进入正确 workspace 的实时流。
|
// 这里从页面 meta 反查,确保后续 command artifacts 能进入正确 workspace 的实时流。
|
||||||
let meta =
|
let meta =
|
||||||
fetch_documents_meta_via_legacy_cloud(state.config(), context, None, document_id).await?;
|
reject_non_local_documents_meta(state.config(), context, None, document_id).await?;
|
||||||
Ok(read_workspace_id_from_meta(&meta))
|
Ok(read_workspace_id_from_meta(&meta))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execution_artifacts_json(
|
fn execution_artifacts_json(_execution: &Value) -> Value {
|
||||||
execution: &crate::transport::legacy_cloud_guard::RetiredCloudCommandExecution,
|
Value::Null
|
||||||
) -> Value {
|
|
||||||
execution
|
|
||||||
.artifacts
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|artifacts| serde_json::to_value(artifacts).ok())
|
|
||||||
.unwrap_or(Value::Null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_mindmap(
|
pub async fn get_mindmap(
|
||||||
@@ -179,7 +173,7 @@ pub async fn get_mindmap(
|
|||||||
let effective_workspace_id =
|
let effective_workspace_id =
|
||||||
resolve_effective_workspace_id(&context, params.workspace_id.as_deref(), false)?;
|
resolve_effective_workspace_id(&context, params.workspace_id.as_deref(), false)?;
|
||||||
let query_name = resolve_query_name(¶ms);
|
let query_name = resolve_query_name(¶ms);
|
||||||
let result = execute_runtime_query_via_legacy_cloud(
|
let result = reject_non_local_runtime_query(
|
||||||
state.config(),
|
state.config(),
|
||||||
&context,
|
&context,
|
||||||
effective_workspace_id.as_deref(),
|
effective_workspace_id.as_deref(),
|
||||||
@@ -361,7 +355,7 @@ pub async fn apply_mindmap_command(
|
|||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
&context,
|
&context,
|
||||||
effective_workspace_id.as_deref(),
|
effective_workspace_id.as_deref(),
|
||||||
@@ -375,14 +369,14 @@ pub async fn apply_mindmap_command(
|
|||||||
Json(json!({
|
Json(json!({
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"commandName": "mindmaps.put",
|
"commandName": "mindmaps.put",
|
||||||
"result": execution.result,
|
"result": execution,
|
||||||
"artifacts": execution_artifacts_json(&execution),
|
"artifacts": execution_artifacts_json(&execution),
|
||||||
"artifactError": execution.artifact_error,
|
"artifactError": None::<String>,
|
||||||
})),
|
})),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let current = fetch_query_data_via_legacy_cloud(
|
let current = reject_non_local_query_data(
|
||||||
state.config(),
|
state.config(),
|
||||||
&context,
|
&context,
|
||||||
effective_workspace_id.as_deref(),
|
effective_workspace_id.as_deref(),
|
||||||
@@ -443,7 +437,7 @@ pub async fn apply_mindmap_command(
|
|||||||
validate_only: false,
|
validate_only: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
&context,
|
&context,
|
||||||
effective_workspace_id.as_deref(),
|
effective_workspace_id.as_deref(),
|
||||||
@@ -460,9 +454,9 @@ pub async fn apply_mindmap_command(
|
|||||||
"applied": applied.applied,
|
"applied": applied.applied,
|
||||||
"errors": applied.errors,
|
"errors": applied.errors,
|
||||||
"projectionRevision": body.projection_revision,
|
"projectionRevision": body.projection_revision,
|
||||||
"result": execution.result,
|
"result": execution,
|
||||||
"artifacts": execution_artifacts_json(&execution),
|
"artifacts": execution_artifacts_json(&execution),
|
||||||
"artifactError": execution.artifact_error,
|
"artifactError": None::<String>,
|
||||||
})),
|
})),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -482,23 +476,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(
|
|
||||||
r#"{"documents:getMeta":{"id":"doc_1","workspace_id":"ws_demo","title":"页面"},"mindmaps:get":{"data":{"data":{"text":"KMIND","uid":"root"},"children":[]},"revision":1}}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
mutation_fixtures_json: Some(
|
|
||||||
r#"{"mindmaps:put":{"ok":true,"document_id":"doc_1","mindmap_id":"mind_1","updated_at":"2026-05-12T00:00:00Z"},"bridgeLogs:recordCommandLog":{"ok":true,"id":"clog_fixture"},"bridgeLogs:recordDomainEvent":{"ok":true,"id":"evt_fixture"}}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -356,62 +356,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(
|
|
||||||
serde_json::json!({
|
|
||||||
"sidebar:datasetList": {
|
|
||||||
"active_workspace_id": "ws_demo",
|
|
||||||
"documents": [
|
|
||||||
{
|
|
||||||
"id": "doc_1",
|
|
||||||
"workspace_id": "ws_demo",
|
|
||||||
"title": "页面",
|
|
||||||
"parent_id": null,
|
|
||||||
"sort_order": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"media_assets": [],
|
|
||||||
"mindmap_assets": [
|
|
||||||
{
|
|
||||||
"id": "mind_1",
|
|
||||||
"workspace_id": "ws_demo",
|
|
||||||
"document_id": "doc_1",
|
|
||||||
"asset_type": "mindmap",
|
|
||||||
"file_name": "思维导图.json",
|
|
||||||
"mime_type": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"table_assets": [],
|
|
||||||
"trashed_documents": [],
|
|
||||||
"trashed_media_assets": [],
|
|
||||||
"trashed_mindmap_assets": [],
|
|
||||||
"trashed_table_assets": [],
|
|
||||||
"mindmap_docs": [],
|
|
||||||
"mindmap_asset_children": {}
|
|
||||||
},
|
|
||||||
"documents:getMeta": {
|
|
||||||
"id": "doc_1",
|
|
||||||
"workspace_id": "ws_demo",
|
|
||||||
"title": "页面"
|
|
||||||
},
|
|
||||||
"documents:getContent": {
|
|
||||||
"content": [],
|
|
||||||
"revision": 1,
|
|
||||||
"conflict_detection_key": "doc_1:1",
|
|
||||||
"pageSubtree": {"rootNodeId": "doc_1", "outline": []}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -425,36 +374,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(
|
|
||||||
serde_json::json!({
|
|
||||||
"mindmaps:get": {
|
|
||||||
"ok": true,
|
|
||||||
"source": "compat-blob",
|
|
||||||
"revision": 7,
|
|
||||||
"data": {
|
|
||||||
"data": {"uid": "root", "text": "KMIND"},
|
|
||||||
"children": [
|
|
||||||
{"data": {"uid": "topic", "text": "二级节点"}, "children": []}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"document_id": "doc_1",
|
|
||||||
"mindmap_id": "mind_1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -143,30 +143,16 @@ fn local_agent_audit_record_tool_write(
|
|||||||
|
|
||||||
/// 从 profile 本地 config.yaml 读取禁用的 mnote tools。
|
/// 从 profile 本地 config.yaml 读取禁用的 mnote tools。
|
||||||
/// 路径:`$MNOTE_AGENT_HOME/profiles/{profile}/config.yaml` 中 `mnote.tools.disabled`。
|
/// 路径:`$MNOTE_AGENT_HOME/profiles/{profile}/config.yaml` 中 `mnote.tools.disabled`。
|
||||||
/// 兼容读取:`HERMES_HOME` / `~/.hermes`(仅 profile 配置路径,非产品面)。
|
/// 默认回落 `~/.mnote-agent`(不再读取 `HERMES_HOME` / `~/.hermes`)。
|
||||||
fn agent_profile_home() -> PathBuf {
|
fn agent_profile_home() -> PathBuf {
|
||||||
env::var("MNOTE_AGENT_HOME")
|
env::var("MNOTE_AGENT_HOME")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|value| !value.trim().is_empty())
|
.filter(|value| !value.trim().is_empty())
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
env::var("HERMES_HOME")
|
env::var("HOME")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|value| !value.trim().is_empty())
|
.map(|home| PathBuf::from(home).join(".mnote-agent"))
|
||||||
.map(PathBuf::from)
|
|
||||||
})
|
|
||||||
.or_else(|| {
|
|
||||||
env::var("HOME").ok().and_then(|home| {
|
|
||||||
let preferred = PathBuf::from(&home).join(".mnote-agent");
|
|
||||||
if preferred.exists() {
|
|
||||||
return Some(preferred);
|
|
||||||
}
|
|
||||||
let legacy = PathBuf::from(&home).join(".hermes");
|
|
||||||
if legacy.exists() {
|
|
||||||
return Some(legacy);
|
|
||||||
}
|
|
||||||
Some(preferred)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| PathBuf::from(".mnote-agent"))
|
.unwrap_or_else(|| PathBuf::from(".mnote-agent"))
|
||||||
}
|
}
|
||||||
@@ -1118,13 +1104,6 @@ fn audit_log_path() -> PathBuf {
|
|||||||
.ok()
|
.ok()
|
||||||
.filter(|value| !value.trim().is_empty())
|
.filter(|value| !value.trim().is_empty())
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.or_else(|| {
|
|
||||||
// 兼容旧 env 名
|
|
||||||
env::var("MNOTE_HERMES_TOOL_AUDIT_LOG")
|
|
||||||
.ok()
|
|
||||||
.filter(|value| !value.trim().is_empty())
|
|
||||||
.map(PathBuf::from)
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| PathBuf::from("tmp").join("mnote-agent-tool-audit.jsonl"))
|
.unwrap_or_else(|| PathBuf::from("tmp").join("mnote-agent-tool-audit.jsonl"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1394,61 +1373,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"documents:getMeta": {
|
|
||||||
"id": "doc_1",
|
|
||||||
"workspace_id": "ws_demo",
|
|
||||||
"title": "服务端页面",
|
|
||||||
"can_edit": true,
|
|
||||||
"wide_layout": false,
|
|
||||||
"use_small_text": false,
|
|
||||||
"show_toc": true,
|
|
||||||
"block_count": 1
|
|
||||||
},
|
|
||||||
"documents:getContent": {
|
|
||||||
"title": "服务端页面",
|
|
||||||
"content": [
|
|
||||||
{
|
|
||||||
"id": "heading_1",
|
|
||||||
"type": "heading",
|
|
||||||
"props": { "level": 1 },
|
|
||||||
"content": [{ "type": "text", "text": "章节一" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "p_1",
|
|
||||||
"type": "paragraph",
|
|
||||||
"content": [{ "type": "text", "text": "第一段" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "p_2",
|
|
||||||
"type": "paragraph",
|
|
||||||
"content": [{ "type": "text", "text": "第二段" }]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"revision": 7,
|
|
||||||
"conflict_detection_key": "doc_1:7"
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
mutation_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"documents:updateContent": {"ok": true, "revision": 8, "conflict_detection_key": "doc_1:8"},
|
|
||||||
"bridgeLogs:recordCommandLog": {"ok": true, "id": "cmd_log_fixture"},
|
|
||||||
"bridgeLogs:recordDomainEvent": {"ok": true, "id": "event_fixture"}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -1462,44 +1391,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(
|
|
||||||
json!({
|
|
||||||
"documents:getMeta": {
|
|
||||||
"id": "doc_1",
|
|
||||||
"workspace_id": "ws_demo",
|
|
||||||
"title": "复杂块阻断页面",
|
|
||||||
"can_edit": true,
|
|
||||||
"wide_layout": false,
|
|
||||||
"use_small_text": false,
|
|
||||||
"show_toc": true,
|
|
||||||
"block_count": 6
|
|
||||||
},
|
|
||||||
"documents:getContent": {
|
|
||||||
"title": "复杂块阻断页面",
|
|
||||||
"content": content,
|
|
||||||
"revision": 7,
|
|
||||||
"conflict_detection_key": "doc_1:7"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
mutation_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"documents:updateContent": {"ok": true, "revision": 8, "conflict_detection_key": "doc_1:8"},
|
|
||||||
"bridgeLogs:recordCommandLog": {"ok": true, "id": "cmd_log_fixture"},
|
|
||||||
"bridgeLogs:recordDomainEvent": {"ok": true, "id": "event_fixture"}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ mod tree;
|
|||||||
mod tree_view_state;
|
mod tree_view_state;
|
||||||
mod ui_debug;
|
mod ui_debug;
|
||||||
pub(crate) mod ui_preferences;
|
pub(crate) mod ui_preferences;
|
||||||
mod vault;
|
pub(crate) mod vault;
|
||||||
pub(crate) mod vault_extension_token;
|
pub(crate) mod vault_extension_token;
|
||||||
mod vault_path;
|
mod vault_path;
|
||||||
mod vault_store;
|
mod vault_store;
|
||||||
@@ -893,17 +893,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes,
|
enable_debug_shell_routes,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: false,
|
allow_dev_fixtures: false,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -1056,7 +1050,7 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("body bytes");
|
.expect("body bytes");
|
||||||
let payload: Value = serde_json::from_slice(&body).expect("json body");
|
let payload: Value = serde_json::from_slice(&body).expect("json body");
|
||||||
assert_eq!(payload["code"], "mnote_media_convex_retired");
|
assert_eq!(payload["code"], "mnote_media_local_required");
|
||||||
assert_eq!(payload["operation"], operation);
|
assert_eq!(payload["operation"], operation);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["replacement"]["upload"],
|
payload["replacement"]["upload"],
|
||||||
|
|||||||
@@ -367,17 +367,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use super::onlyoffice_bridge;
|
use super::onlyoffice_bridge;
|
||||||
use crate::app::AppConfig;
|
|
||||||
use crate::app::AppState;
|
use crate::app::AppState;
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use adapter_onlyoffice::{
|
use adapter_onlyoffice::{
|
||||||
@@ -921,7 +920,7 @@ pub async fn proxy(
|
|||||||
onlyoffice_storage_host_override: env_or_dotenv(
|
onlyoffice_storage_host_override: env_or_dotenv(
|
||||||
"NEXT_PUBLIC_ONLYOFFICE_STORAGE_HOST_OVERRIDE",
|
"NEXT_PUBLIC_ONLYOFFICE_STORAGE_HOST_OVERRIDE",
|
||||||
),
|
),
|
||||||
convex_origin: None,
|
extra_origin: None,
|
||||||
supabase_anon_key: env_or_dotenv("NEXT_PUBLIC_SUPABASE_ANON_KEY")
|
supabase_anon_key: env_or_dotenv("NEXT_PUBLIC_SUPABASE_ANON_KEY")
|
||||||
.or_else(|| env_or_dotenv("SUPABASE_ANON_KEY")),
|
.or_else(|| env_or_dotenv("SUPABASE_ANON_KEY")),
|
||||||
})
|
})
|
||||||
@@ -1417,41 +1416,23 @@ pub async fn callback(
|
|||||||
Err(error) => onlyoffice_callback_failure(error),
|
Err(error) => onlyoffice_callback_failure(error),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
match proxy_legacy_onlyoffice_json(
|
// local-first:非 local asset 写回已退役 Next 代理;只服务 local-folder callback。
|
||||||
state.config(),
|
let _ = (state, uri, body);
|
||||||
"/api/onlyoffice/callback",
|
(
|
||||||
uri.query(),
|
axum::http::StatusCode::NOT_IMPLEMENTED,
|
||||||
Some(body),
|
Json(json!({
|
||||||
|
"error": 1,
|
||||||
|
"degraded": true,
|
||||||
|
"code": "onlyoffice_cloud_writeback_unavailable",
|
||||||
|
"message": "OnlyOffice 非 local asset 写回已退役;请使用 local-folder 路径(rootUri/path 或 local-file asset)。",
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.await
|
.into_response()
|
||||||
{
|
|
||||||
Ok(response) => response,
|
|
||||||
Err(error) if error.status() == axum::http::StatusCode::NOT_IMPLEMENTED => (
|
|
||||||
axum::http::StatusCode::NOT_IMPLEMENTED,
|
|
||||||
Json(json!({
|
|
||||||
"error": 1,
|
|
||||||
"degraded": true,
|
|
||||||
"code": "onlyoffice_legacy_writeback_unavailable",
|
|
||||||
"message": error.message(),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
Err(error) => (
|
|
||||||
axum::http::StatusCode::BAD_GATEWAY,
|
|
||||||
Json(json!({
|
|
||||||
"error": 1,
|
|
||||||
"degraded": true,
|
|
||||||
"code": "onlyoffice_legacy_writeback_failed",
|
|
||||||
"message": error.message(),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn forcesave(
|
pub async fn forcesave(
|
||||||
State(state): State<AppState>,
|
State(_state): State<AppState>,
|
||||||
uri: Uri,
|
_uri: Uri,
|
||||||
Query(query): Query<OnlyOfficeForcesaveQuery>,
|
Query(query): Query<OnlyOfficeForcesaveQuery>,
|
||||||
) -> Result<Response, WebError> {
|
) -> Result<Response, WebError> {
|
||||||
let asset_id = query
|
let asset_id = query
|
||||||
@@ -1470,104 +1451,14 @@ pub async fn forcesave(
|
|||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
WebError::bad_request_code("onlyoffice_forcesave_key_missing", "缺少 key")
|
WebError::bad_request_code("onlyoffice_forcesave_key_missing", "缺少 key")
|
||||||
})?;
|
})?;
|
||||||
let response = proxy_legacy_onlyoffice_json(
|
// local-first:forcesave 的 Next 写回链已退役;非 local 路径显式不可用。
|
||||||
state.config(),
|
Err(WebError::new(
|
||||||
"/api/onlyoffice/forcesave",
|
axum::http::StatusCode::NOT_IMPLEMENTED,
|
||||||
uri.query(),
|
"onlyoffice_cloud_writeback_unavailable",
|
||||||
None,
|
format!(
|
||||||
)
|
"OnlyOffice forcesave 仅支持 local-folder 主链;云/兼容写回已退役: assetId={asset_id}, key={key}"
|
||||||
.await
|
),
|
||||||
.map_err(|error| {
|
))
|
||||||
if error.status() == axum::http::StatusCode::NOT_IMPLEMENTED {
|
|
||||||
return WebError::new(
|
|
||||||
axum::http::StatusCode::NOT_IMPLEMENTED,
|
|
||||||
"onlyoffice_legacy_writeback_unavailable",
|
|
||||||
format!(
|
|
||||||
"OnlyOffice forcesave 未配置 legacy Next 写回链: assetId={asset_id}, key={key}"
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
error
|
|
||||||
})?;
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn legacy_onlyoffice_writeback_base(config: &AppConfig) -> Option<String> {
|
|
||||||
config
|
|
||||||
.legacy_next_base_url
|
|
||||||
.as_deref()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(|value| value.trim_end_matches('/').to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn proxy_legacy_onlyoffice_json(
|
|
||||||
config: &AppConfig,
|
|
||||||
path: &str,
|
|
||||||
query: Option<&str>,
|
|
||||||
body: Option<Value>,
|
|
||||||
) -> Result<Response, WebError> {
|
|
||||||
let base = legacy_onlyoffice_writeback_base(config).ok_or_else(|| {
|
|
||||||
WebError::new(
|
|
||||||
axum::http::StatusCode::NOT_IMPLEMENTED,
|
|
||||||
"onlyoffice_legacy_writeback_unavailable",
|
|
||||||
"OnlyOffice Rust route 暂未直接写回,且未配置 legacy Next 写回链",
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let target = append_path_and_query(&base, path, query);
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(Duration::from_secs(30))
|
|
||||||
.build()
|
|
||||||
.map_err(|error| {
|
|
||||||
WebError::internal(format!(
|
|
||||||
"OnlyOffice legacy proxy HTTP 客户端创建失败: {error}"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
let mut request = client
|
|
||||||
.post(target)
|
|
||||||
.header(header::CONTENT_TYPE, "application/json");
|
|
||||||
if let Some(body) = body {
|
|
||||||
request = request.json(&body);
|
|
||||||
} else {
|
|
||||||
request = request.body("{}");
|
|
||||||
}
|
|
||||||
let upstream = request.send().await.map_err(|error| {
|
|
||||||
WebError::bad_gateway_code(
|
|
||||||
"onlyoffice_legacy_writeback_failed",
|
|
||||||
format!("OnlyOffice legacy 写回请求失败: {error}"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let status = upstream.status();
|
|
||||||
let content_type = upstream
|
|
||||||
.headers()
|
|
||||||
.get(header::CONTENT_TYPE)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| HeaderValue::from_static("application/json"));
|
|
||||||
let bytes = upstream.bytes().await.map_err(|error| {
|
|
||||||
WebError::bad_gateway_code(
|
|
||||||
"onlyoffice_legacy_writeback_failed",
|
|
||||||
format!("OnlyOffice legacy 写回响应读取失败: {error}"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let mut response = Response::new(Body::from(bytes));
|
|
||||||
*response.status_mut() = status;
|
|
||||||
response
|
|
||||||
.headers_mut()
|
|
||||||
.insert(header::CONTENT_TYPE, content_type);
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_path_and_query(base: &str, path: &str, query: Option<&str>) -> String {
|
|
||||||
let mut target = format!(
|
|
||||||
"{}/{}",
|
|
||||||
base.trim_end_matches('/'),
|
|
||||||
path.trim_start_matches('/')
|
|
||||||
);
|
|
||||||
if let Some(query) = query.filter(|value| !value.is_empty()) {
|
|
||||||
target.push('?');
|
|
||||||
target.push_str(query);
|
|
||||||
}
|
|
||||||
target
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_body_bytes(
|
fn request_body_bytes(
|
||||||
@@ -1613,6 +1504,19 @@ fn strip_hop_by_hop_headers(headers: &mut HeaderMap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn append_path_and_query(base: &str, path: &str, query: Option<&str>) -> String {
|
||||||
|
let mut target = format!(
|
||||||
|
"{}/{}",
|
||||||
|
base.trim_end_matches('/'),
|
||||||
|
path.trim_start_matches('/')
|
||||||
|
);
|
||||||
|
if let Some(query) = query.filter(|value| !value.is_empty()) {
|
||||||
|
target.push('?');
|
||||||
|
target.push_str(query);
|
||||||
|
}
|
||||||
|
target
|
||||||
|
}
|
||||||
|
|
||||||
async fn proxy_onlyoffice_path(
|
async fn proxy_onlyoffice_path(
|
||||||
upstream_prefix: &str,
|
upstream_prefix: &str,
|
||||||
upstream_path: &str,
|
upstream_path: &str,
|
||||||
@@ -2067,23 +1971,16 @@ mod tests {
|
|||||||
assert!(html.contains("window.__MNOTE_ONLYOFFICE_BRIDGE__"));
|
assert!(html.contains("window.__MNOTE_ONLYOFFICE_BRIDGE__"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_state(legacy_next_base_url: Option<String>) -> AppState {
|
fn test_state() -> AppState {
|
||||||
AppState::new(AppConfig {
|
AppState::new(AppConfig {
|
||||||
service_name: "mnote-web".into(),
|
service_name: "mnote-web".into(),
|
||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url,
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: false,
|
allow_dev_fixtures: false,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -2091,7 +1988,7 @@ mod tests {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn spawn_legacy_json_server(
|
async fn spawn_mock_download_server(
|
||||||
response_body: &'static str,
|
response_body: &'static str,
|
||||||
) -> (String, oneshot::Receiver<String>) {
|
) -> (String, oneshot::Receiver<String>) {
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
||||||
@@ -2104,7 +2001,7 @@ mod tests {
|
|||||||
let request = String::from_utf8_lossy(&buffer[..read]).to_string();
|
let request = String::from_utf8_lossy(&buffer[..read]).to_string();
|
||||||
let _ = tx.send(request);
|
let _ = tx.send(request);
|
||||||
let response = format!(
|
let response = format!(
|
||||||
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
|
"HTTP/1.1 200 OK\r\ncontent-type: application/octet-stream\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
|
||||||
response_body.len(),
|
response_body.len(),
|
||||||
response_body
|
response_body
|
||||||
);
|
);
|
||||||
@@ -2114,9 +2011,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn onlyoffice_callback_without_legacy_next_fails_explicitly() {
|
async fn onlyoffice_callback_without_local_asset_fails_explicitly() {
|
||||||
let response = callback(
|
let response = callback(
|
||||||
State(test_state(None)),
|
State(test_state()),
|
||||||
"/api/onlyoffice/callback?assetId=asset_1"
|
"/api/onlyoffice/callback?assetId=asset_1"
|
||||||
.parse::<Uri>()
|
.parse::<Uri>()
|
||||||
.expect("uri"),
|
.expect("uri"),
|
||||||
@@ -2155,9 +2052,9 @@ mod tests {
|
|||||||
fs::create_dir_all(root.join("Page")).expect("create page");
|
fs::create_dir_all(root.join("Page")).expect("create page");
|
||||||
let target = root.join("Page").join("report.docx");
|
let target = root.join("Page").join("report.docx");
|
||||||
fs::write(&target, b"old").expect("write old docx");
|
fs::write(&target, b"old").expect("write old docx");
|
||||||
let (download_url, _captured) = spawn_legacy_json_server("new docx bytes").await;
|
let (download_url, _captured) = spawn_mock_download_server("new docx bytes").await;
|
||||||
let response = callback(
|
let response = callback(
|
||||||
State(test_state(None)),
|
State(test_state()),
|
||||||
format!(
|
format!(
|
||||||
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
||||||
root.display()
|
root.display()
|
||||||
@@ -2205,7 +2102,7 @@ mod tests {
|
|||||||
fs::create_dir_all(root.join("Page")).expect("create page");
|
fs::create_dir_all(root.join("Page")).expect("create page");
|
||||||
let target = root.join("Page").join("report.docx");
|
let target = root.join("Page").join("report.docx");
|
||||||
fs::write(&target, b"old").expect("write old docx");
|
fs::write(&target, b"old").expect("write old docx");
|
||||||
let (download_url, _captured) = spawn_legacy_json_server("new docx bytes").await;
|
let (download_url, _captured) = spawn_mock_download_server("new docx bytes").await;
|
||||||
let session_id = format!("mnote-oo-local-callback-{}", std::process::id());
|
let session_id = format!("mnote-oo-local-callback-{}", std::process::id());
|
||||||
let token = format!("token-{session_id}");
|
let token = format!("token-{session_id}");
|
||||||
let registered =
|
let registered =
|
||||||
@@ -2222,7 +2119,7 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
assert_eq!(registered.status(), StatusCode::OK);
|
assert_eq!(registered.status(), StatusCode::OK);
|
||||||
let response = callback(
|
let response = callback(
|
||||||
State(test_state(None)),
|
State(test_state()),
|
||||||
format!(
|
format!(
|
||||||
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
||||||
session_id,
|
session_id,
|
||||||
@@ -2267,7 +2164,7 @@ mod tests {
|
|||||||
fs::create_dir_all(root.join("Page")).expect("create page");
|
fs::create_dir_all(root.join("Page")).expect("create page");
|
||||||
let target = root.join("Page").join("report.docx");
|
let target = root.join("Page").join("report.docx");
|
||||||
fs::write(&target, b"old").expect("write old docx");
|
fs::write(&target, b"old").expect("write old docx");
|
||||||
let (download_url, _captured) = spawn_legacy_json_server("status six bytes").await;
|
let (download_url, _captured) = spawn_mock_download_server("status six bytes").await;
|
||||||
let session_id = format!("mnote-oo-local-callback-six-{}", std::process::id());
|
let session_id = format!("mnote-oo-local-callback-six-{}", std::process::id());
|
||||||
let token = format!("token-{session_id}");
|
let token = format!("token-{session_id}");
|
||||||
let registered =
|
let registered =
|
||||||
@@ -2284,7 +2181,7 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
assert_eq!(registered.status(), StatusCode::OK);
|
assert_eq!(registered.status(), StatusCode::OK);
|
||||||
let response = callback(
|
let response = callback(
|
||||||
State(test_state(None)),
|
State(test_state()),
|
||||||
format!(
|
format!(
|
||||||
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
||||||
session_id,
|
session_id,
|
||||||
@@ -2334,7 +2231,7 @@ mod tests {
|
|||||||
fs::create_dir_all(root.join("Page")).expect("create page");
|
fs::create_dir_all(root.join("Page")).expect("create page");
|
||||||
fs::create_dir_all(&outside).expect("create outside");
|
fs::create_dir_all(&outside).expect("create outside");
|
||||||
fs::write(outside.join("report.docx"), b"outside").expect("write outside");
|
fs::write(outside.join("report.docx"), b"outside").expect("write outside");
|
||||||
let (download_url, _captured) = spawn_legacy_json_server("should not write").await;
|
let (download_url, _captured) = spawn_mock_download_server("should not write").await;
|
||||||
let session_id = format!("mnote-oo-local-callback-escape-{}", std::process::id());
|
let session_id = format!("mnote-oo-local-callback-escape-{}", std::process::id());
|
||||||
let token = format!("token-{session_id}");
|
let token = format!("token-{session_id}");
|
||||||
let registered =
|
let registered =
|
||||||
@@ -2351,7 +2248,7 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
assert_eq!(registered.status(), StatusCode::OK);
|
assert_eq!(registered.status(), StatusCode::OK);
|
||||||
let response = callback(
|
let response = callback(
|
||||||
State(test_state(None)),
|
State(test_state()),
|
||||||
format!(
|
format!(
|
||||||
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=..%2F{}%2Freport.docx",
|
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=..%2F{}%2Freport.docx",
|
||||||
session_id,
|
session_id,
|
||||||
@@ -2426,7 +2323,7 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
assert_eq!(registered.status(), StatusCode::OK);
|
assert_eq!(registered.status(), StatusCode::OK);
|
||||||
let response = callback(
|
let response = callback(
|
||||||
State(test_state(None)),
|
State(test_state()),
|
||||||
format!(
|
format!(
|
||||||
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
||||||
session_id,
|
session_id,
|
||||||
@@ -2458,46 +2355,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn onlyoffice_callback_proxies_to_legacy_next_writeback() {
|
async fn onlyoffice_forcesave_without_local_writeback_is_not_noop_success() {
|
||||||
let (base_url, captured) = spawn_legacy_json_server(r#"{"error":0}"#).await;
|
|
||||||
let response = callback(
|
|
||||||
State(test_state(Some(base_url))),
|
|
||||||
"/api/onlyoffice/callback?assetId=asset_1&userId=user_1"
|
|
||||||
.parse::<Uri>()
|
|
||||||
.expect("uri"),
|
|
||||||
HeaderMap::new(),
|
|
||||||
Query(OnlyOfficeCallbackQuery {
|
|
||||||
asset_id: Some("asset_1".into()),
|
|
||||||
user_id: Some("user_1".into()),
|
|
||||||
session_id: None,
|
|
||||||
token: None,
|
|
||||||
root_uri: None,
|
|
||||||
path: None,
|
|
||||||
}),
|
|
||||||
Json(json!({
|
|
||||||
"status": 2,
|
|
||||||
"key": "doc_key",
|
|
||||||
"url": "http://127.0.0.1:8082/cache/files/out.docx"
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
|
||||||
.await
|
|
||||||
.expect("body");
|
|
||||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
|
||||||
let request = captured.await.expect("captured");
|
|
||||||
|
|
||||||
assert_eq!(payload["error"], 0);
|
|
||||||
assert!(request
|
|
||||||
.starts_with("POST /api/onlyoffice/callback?assetId=asset_1&userId=user_1 HTTP/1.1"));
|
|
||||||
assert!(request.contains(r#""status":2"#));
|
|
||||||
assert!(request.contains(r#""key":"doc_key""#));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn onlyoffice_forcesave_without_legacy_next_is_not_noop_success() {
|
|
||||||
let response = forcesave(
|
let response = forcesave(
|
||||||
State(test_state(None)),
|
State(test_state()),
|
||||||
"/api/onlyoffice/forcesave?assetId=asset_1&key=doc_key"
|
"/api/onlyoffice/forcesave?assetId=asset_1&key=doc_key"
|
||||||
.parse::<Uri>()
|
.parse::<Uri>()
|
||||||
.expect("uri"),
|
.expect("uri"),
|
||||||
@@ -2516,38 +2376,10 @@ mod tests {
|
|||||||
.expect("body");
|
.expect("body");
|
||||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||||
assert_eq!(payload["ok"], false);
|
assert_eq!(payload["ok"], false);
|
||||||
assert_eq!(payload["code"], "onlyoffice_legacy_writeback_unavailable");
|
assert_eq!(payload["code"], "onlyoffice_cloud_writeback_unavailable");
|
||||||
assert_ne!(payload["via"], "mnote-web-rust-noop");
|
assert_ne!(payload["via"], "mnote-web-rust-noop");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn onlyoffice_forcesave_proxies_to_legacy_next_writeback() {
|
|
||||||
let (base_url, captured) =
|
|
||||||
spawn_legacy_json_server(r#"{"ok":true,"via":"forcesave","result":{"error":0}}"#).await;
|
|
||||||
let response = forcesave(
|
|
||||||
State(test_state(Some(base_url))),
|
|
||||||
"/api/onlyoffice/forcesave?assetId=asset_1&key=doc_key"
|
|
||||||
.parse::<Uri>()
|
|
||||||
.expect("uri"),
|
|
||||||
Query(OnlyOfficeForcesaveQuery {
|
|
||||||
asset_id: Some("asset_1".into()),
|
|
||||||
key: Some("doc_key".into()),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("response");
|
|
||||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
|
||||||
.await
|
|
||||||
.expect("body");
|
|
||||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
|
||||||
let request = captured.await.expect("captured");
|
|
||||||
|
|
||||||
assert_eq!(payload["ok"], true);
|
|
||||||
assert_eq!(payload["via"], "forcesave");
|
|
||||||
assert!(request
|
|
||||||
.starts_with("POST /api/onlyoffice/forcesave?assetId=asset_1&key=doc_key HTTP/1.1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn onlyoffice_page_skips_media_sign_for_local_folder_asset() {
|
async fn onlyoffice_page_skips_media_sign_for_local_folder_asset() {
|
||||||
let response = page(Query(OnlyOfficePageQuery {
|
let response = page(Query(OnlyOfficePageQuery {
|
||||||
@@ -2626,7 +2458,7 @@ mod tests {
|
|||||||
async fn onlyoffice_callback_rejects_invalid_jwt_when_secret_configured() {
|
async fn onlyoffice_callback_rejects_invalid_jwt_when_secret_configured() {
|
||||||
std::env::set_var("ONLYOFFICE_JWT_SECRET", "test-onlyoffice-jwt-secret");
|
std::env::set_var("ONLYOFFICE_JWT_SECRET", "test-onlyoffice-jwt-secret");
|
||||||
let response = callback(
|
let response = callback(
|
||||||
State(test_state(None)),
|
State(test_state()),
|
||||||
"/api/onlyoffice/callback?assetId=asset_1"
|
"/api/onlyoffice/callback?assetId=asset_1"
|
||||||
.parse::<Uri>()
|
.parse::<Uri>()
|
||||||
.expect("uri"),
|
.expect("uri"),
|
||||||
@@ -2679,5 +2511,5 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let _ = fs::remove_dir_all(&root);
|
let _ = fs::remove_dir_all(&root);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9218,17 +9218,11 @@ mod tests {
|
|||||||
service_version: "test".into(),
|
service_version: "test".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: None,
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: true,
|
enable_page_ai_pi_lab: true,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -583,23 +583,9 @@ fn agent_profile_home() -> PathBuf {
|
|||||||
.filter(|value| !value.trim().is_empty())
|
.filter(|value| !value.trim().is_empty())
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
std::env::var("HERMES_HOME")
|
std::env::var("HOME")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|value| !value.trim().is_empty())
|
.map(|home| PathBuf::from(home).join(".mnote-agent"))
|
||||||
.map(PathBuf::from)
|
|
||||||
})
|
|
||||||
.or_else(|| {
|
|
||||||
std::env::var("HOME").ok().and_then(|home| {
|
|
||||||
let preferred = PathBuf::from(&home).join(".mnote-agent");
|
|
||||||
if preferred.exists() {
|
|
||||||
return Some(preferred);
|
|
||||||
}
|
|
||||||
let legacy = PathBuf::from(&home).join(".hermes");
|
|
||||||
if legacy.exists() {
|
|
||||||
return Some(legacy);
|
|
||||||
}
|
|
||||||
Some(preferred)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| PathBuf::from(".mnote-agent"))
|
.unwrap_or_else(|| PathBuf::from(".mnote-agent"))
|
||||||
}
|
}
|
||||||
@@ -716,55 +702,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"documents:getMeta": {
|
|
||||||
"id": "doc_1",
|
|
||||||
"workspace_id": "ws_demo",
|
|
||||||
"title": "服务端页面",
|
|
||||||
"can_edit": true,
|
|
||||||
"wide_layout": false,
|
|
||||||
"use_small_text": false,
|
|
||||||
"show_toc": true,
|
|
||||||
"block_count": 2
|
|
||||||
},
|
|
||||||
"documents:getContent": {
|
|
||||||
"title": "服务端页面",
|
|
||||||
"content": [
|
|
||||||
{
|
|
||||||
"id": "p_1",
|
|
||||||
"type": "paragraph",
|
|
||||||
"content": [{ "type": "text", "text": "第一段" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "p_2",
|
|
||||||
"type": "paragraph",
|
|
||||||
"content": [{ "type": "text", "text": "第二段" }]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"revision": 7,
|
|
||||||
"conflict_detection_key": "doc_1:7"
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
mutation_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"documents:updateContent": {"ok": true, "revision": 8, "conflict_detection_key": "doc_1:8"},
|
|
||||||
"bridgeLogs:recordCommandLog": {"ok": true, "id": "cmd_log_fixture"},
|
|
||||||
"bridgeLogs:recordDomainEvent": {"ok": true, "id": "event_fixture"}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::app::AppConfig;
|
use crate::app::AppConfig;
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::transport::legacy_cloud_guard::execute_retired_query_plan;
|
use crate::transport::local_only::local_required;
|
||||||
use bridge_runtime::{
|
use bridge_runtime::{
|
||||||
build_query_request, execute_runtime_input, execute_runtime_query, BridgeContext,
|
build_query_request, execute_runtime_input, execute_runtime_query, BridgeContext,
|
||||||
RuntimeActorWire, RuntimeBridgeContextWire, RuntimeExecutionPlan, RuntimeInput,
|
RuntimeActorWire, RuntimeBridgeContextWire, RuntimeExecutionPlan, RuntimeInput,
|
||||||
@@ -154,24 +154,24 @@ pub fn build_documents_meta_query_plan(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_query_data_via_legacy_cloud(
|
/// 非 local_folder 的 query data 拉取已移除。
|
||||||
config: &AppConfig,
|
pub async fn reject_non_local_query_data(
|
||||||
|
_config: &AppConfig,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
effective_workspace_id: Option<&str>,
|
_effective_workspace_id: Option<&str>,
|
||||||
query: RuntimeQueryEnvelopeWire,
|
_query: RuntimeQueryEnvelopeWire,
|
||||||
) -> Result<Value, WebError> {
|
) -> Result<Value, WebError> {
|
||||||
let plan = build_runtime_query_plan(context, effective_workspace_id, query)?;
|
Err(local_required(context, "query_local_required"))
|
||||||
execute_retired_query_plan(config, context, &plan).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_documents_meta_via_legacy_cloud(
|
/// 非 local_folder 的 documents.meta 已移除。
|
||||||
config: &AppConfig,
|
pub async fn reject_non_local_documents_meta(
|
||||||
|
_config: &AppConfig,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
effective_workspace_id: Option<&str>,
|
_effective_workspace_id: Option<&str>,
|
||||||
document_id: &str,
|
_document_id: &str,
|
||||||
) -> Result<Value, WebError> {
|
) -> Result<Value, WebError> {
|
||||||
let plan = build_documents_meta_query_plan(context, effective_workspace_id, document_id)?;
|
Err(local_required(context, "query_local_required"))
|
||||||
execute_retired_query_plan(config, context, &plan).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_runtime_query_against_data(
|
pub fn execute_runtime_query_against_data(
|
||||||
@@ -188,14 +188,12 @@ pub fn execute_runtime_query_against_data(
|
|||||||
.map_err(|error| WebError::bad_request(error.message).with_context(context))
|
.map_err(|error| WebError::bad_request(error.message).with_context(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute_runtime_query_via_legacy_cloud(
|
/// 非 local_folder 的 runtime query 一律拒绝。
|
||||||
config: &AppConfig,
|
pub async fn reject_non_local_runtime_query(
|
||||||
|
_config: &AppConfig,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
effective_workspace_id: Option<&str>,
|
_effective_workspace_id: Option<&str>,
|
||||||
query: RuntimeQueryEnvelopeWire,
|
_query: RuntimeQueryEnvelopeWire,
|
||||||
) -> Result<Value, WebError> {
|
) -> Result<Value, WebError> {
|
||||||
let data =
|
Err(local_required(context, "query_local_required"))
|
||||||
fetch_query_data_via_legacy_cloud(config, context, effective_workspace_id, query.clone())
|
|
||||||
.await?;
|
|
||||||
execute_runtime_query_against_data(context, effective_workspace_id, query, data)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,17 @@ use crate::app::AppState;
|
|||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::routes::command_support::{
|
use crate::routes::command_support::{
|
||||||
execute_runtime_command_via_legacy_cloud_with_artifacts, runtime_context,
|
reject_non_local_runtime_command_with_artifacts, runtime_context,
|
||||||
};
|
};
|
||||||
use crate::routes::local_folder_source::{
|
use crate::routes::local_folder_source::{
|
||||||
ensure_local_workspace_access, execute_local_tree_command,
|
ensure_local_workspace_access, execute_local_tree_command,
|
||||||
};
|
};
|
||||||
use crate::transport::legacy_cloud_guard::{
|
use crate::transport::local_only::local_required;
|
||||||
execute_retired_mutation_by_name, execute_retired_query_by_name,
|
|
||||||
persist_runtime_command_artifacts,
|
|
||||||
};
|
|
||||||
use axum::extract::{Extension, Path, Query, State};
|
use axum::extract::{Extension, Path, Query, State};
|
||||||
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use bridge_runtime::{
|
use bridge_runtime::{
|
||||||
build_runtime_command_artifact_plan, RuntimeActorWire, RuntimeCommandEnvelopeWire,
|
RuntimeActorWire, RuntimeCommandEnvelopeWire,
|
||||||
RuntimeCommandExecutionPlan, RuntimeSourceWire, RuntimeTargetWire,
|
RuntimeCommandExecutionPlan, RuntimeSourceWire, RuntimeTargetWire,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -26,6 +23,23 @@ use time::{Duration, OffsetDateTime};
|
|||||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||||
const HEADER_RESOURCE_TRASH_TRANSPORT: &str = "x-mnote-resource-trash-transport";
|
const HEADER_RESOURCE_TRASH_TRANSPORT: &str = "x-mnote-resource-trash-transport";
|
||||||
|
|
||||||
|
async fn reject_cloud_query(
|
||||||
|
context: &RequestContext,
|
||||||
|
error_phase: &'static str,
|
||||||
|
) -> Result<Value, WebError> {
|
||||||
|
Err(local_required(context, error_phase))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reject_cloud_mutation(
|
||||||
|
context: &RequestContext,
|
||||||
|
error_phase: &'static str,
|
||||||
|
) -> Result<Value, WebError> {
|
||||||
|
Err(local_required(context, error_phase))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MediaBatchRequest {
|
pub struct MediaBatchRequest {
|
||||||
@@ -115,38 +129,6 @@ async fn current_user_id(
|
|||||||
return Ok(user_id.to_string());
|
return Ok(user_id.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_auth_cookie = context
|
|
||||||
.auth
|
|
||||||
.cookie_header
|
|
||||||
.as_deref()
|
|
||||||
.map(|value| {
|
|
||||||
value.contains("__convexAuthJWT=") || value.contains("mnote_web_convex_token=")
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
if has_auth_cookie {
|
|
||||||
if let Ok(user) = execute_retired_query_by_name(
|
|
||||||
state.config(),
|
|
||||||
context,
|
|
||||||
"users:currentUser",
|
|
||||||
json!({}),
|
|
||||||
context.workspace.workspace_id.as_deref(),
|
|
||||||
"resource_trash_current_user",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
for key in ["_id", "id"] {
|
|
||||||
if let Some(user_id) = user
|
|
||||||
.get(key)
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
{
|
|
||||||
return Ok(user_id.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.config().allow_dev_fixtures {
|
if state.config().allow_dev_fixtures {
|
||||||
return Ok(state.config().dev_user_id.clone());
|
return Ok(state.config().dev_user_id.clone());
|
||||||
}
|
}
|
||||||
@@ -190,7 +172,7 @@ fn now_iso_like() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn record_media_empty_trash_artifacts(
|
async fn record_media_empty_trash_artifacts(
|
||||||
state: &AppState,
|
_state: &AppState,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
@@ -270,20 +252,13 @@ async fn record_media_empty_trash_artifacts(
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let runtime_context = runtime_context(context, Some(workspace_id));
|
let runtime_context = runtime_context(context, Some(workspace_id));
|
||||||
if let Some(artifacts) = build_runtime_command_artifact_plan(
|
// cloud artifact persist 已移除;local-first 不写 cloud command log。
|
||||||
&runtime_context,
|
let _ = (runtime_context, command, plan, result);
|
||||||
&command,
|
|
||||||
&plan,
|
|
||||||
result,
|
|
||||||
&now_iso_like(),
|
|
||||||
) {
|
|
||||||
persist_runtime_command_artifacts(state.config(), context, &artifacts).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn record_resource_resync_artifacts(
|
async fn record_resource_resync_artifacts(
|
||||||
state: &AppState,
|
_state: &AppState,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
actor_id: &str,
|
actor_id: &str,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
@@ -371,34 +346,17 @@ async fn record_resource_resync_artifacts(
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let runtime_context = runtime_context(context, Some(workspace_id));
|
let runtime_context = runtime_context(context, Some(workspace_id));
|
||||||
if let Some(artifacts) = build_runtime_command_artifact_plan(
|
// cloud artifact persist 已移除;local-first 不写 cloud command log。
|
||||||
&runtime_context,
|
let _ = (runtime_context, command, plan, result);
|
||||||
&command,
|
|
||||||
&plan,
|
|
||||||
result,
|
|
||||||
&now_iso_like(),
|
|
||||||
) {
|
|
||||||
persist_runtime_command_artifacts(state.config(), context, &artifacts).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_document_workspace_id(
|
async fn fetch_document_workspace_id(
|
||||||
state: &AppState,
|
_state: &AppState,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
document_id: &str,
|
_document_id: &str,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
execute_retired_query_by_name(
|
reject_cloud_query(context, "resource_trash_local_required")
|
||||||
state.config(),
|
|
||||||
context,
|
|
||||||
"documents:getMeta",
|
|
||||||
json!({
|
|
||||||
"id": document_id,
|
|
||||||
"includeDeleted": true,
|
|
||||||
}),
|
|
||||||
context.workspace.workspace_id.as_deref(),
|
|
||||||
"resource_trash_document_workspace",
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|meta| {
|
.and_then(|meta| {
|
||||||
@@ -412,22 +370,12 @@ async fn fetch_document_workspace_id(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_table_meta(
|
async fn fetch_table_meta(
|
||||||
state: &AppState,
|
_state: &AppState,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
user_id: &str,
|
_user_id: &str,
|
||||||
table_id: &str,
|
_table_id: &str,
|
||||||
) -> Option<Value> {
|
) -> Option<Value> {
|
||||||
execute_retired_query_by_name(
|
reject_cloud_query(context, "resource_trash_local_required")
|
||||||
state.config(),
|
|
||||||
context,
|
|
||||||
"tables:get",
|
|
||||||
json!({
|
|
||||||
"userId": user_id,
|
|
||||||
"tableId": table_id,
|
|
||||||
}),
|
|
||||||
context.workspace.workspace_id.as_deref(),
|
|
||||||
"resource_trash_table_meta",
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|value| value.is_object())
|
.filter(|value| value.is_object())
|
||||||
@@ -490,22 +438,12 @@ fn annotate_resource_lifecycle_result(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_media_asset_meta(
|
async fn fetch_media_asset_meta(
|
||||||
state: &AppState,
|
_state: &AppState,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
user_id: &str,
|
_user_id: &str,
|
||||||
asset_id: &str,
|
_asset_id: &str,
|
||||||
) -> Result<Value, WebError> {
|
) -> Result<Value, WebError> {
|
||||||
let assets = execute_retired_query_by_name(
|
let assets = reject_cloud_query(context, "resource_trash_local_required")
|
||||||
state.config(),
|
|
||||||
context,
|
|
||||||
"mediaAssets:listByIds",
|
|
||||||
json!({
|
|
||||||
"userId": user_id,
|
|
||||||
"ids": [asset_id],
|
|
||||||
}),
|
|
||||||
context.workspace.workspace_id.as_deref(),
|
|
||||||
"media_asset_meta",
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
Ok(assets
|
Ok(assets
|
||||||
.as_array()
|
.as_array()
|
||||||
@@ -618,7 +556,7 @@ pub async fn media_batch(
|
|||||||
);
|
);
|
||||||
// 先执行 tree runtime command;成功后再做 legacy media store patch,
|
// 先执行 tree runtime command;成功后再做 legacy media store patch,
|
||||||
// 避免 patch 已生效而 tree 失败时调用方仍看到成功(与 restore/delete 一致)。
|
// 避免 patch 已生效而 tree 失败时调用方仍看到成功(与 restore/delete 一致)。
|
||||||
execute_runtime_command_via_legacy_cloud_with_artifacts(
|
reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
&context,
|
&context,
|
||||||
workspace_id.as_deref(),
|
workspace_id.as_deref(),
|
||||||
@@ -626,49 +564,21 @@ pub async fn media_batch(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
if action == "rename" {
|
if action == "rename" {
|
||||||
let new_name = require_id(
|
let _new_name = require_id(
|
||||||
&context,
|
&context,
|
||||||
body.new_name.as_deref().unwrap_or_default(),
|
body.new_name.as_deref().unwrap_or_default(),
|
||||||
"newName",
|
"newName",
|
||||||
)?;
|
)?;
|
||||||
execute_retired_mutation_by_name(
|
reject_cloud_mutation(&context, "")
|
||||||
state.config(),
|
|
||||||
&context,
|
|
||||||
"mediaAssets:patchById",
|
|
||||||
json!({
|
|
||||||
"userId": user_id,
|
|
||||||
"id": asset_id,
|
|
||||||
"patch": {
|
|
||||||
"file_name": new_name,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
workspace_id.as_deref(),
|
|
||||||
None,
|
|
||||||
"media_batch_rename_patch",
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
if action == "move" {
|
if action == "move" {
|
||||||
let target_document_id = require_id(
|
let _target_document_id = require_id(
|
||||||
&context,
|
&context,
|
||||||
body.target_document_id.as_deref().unwrap_or_default(),
|
body.target_document_id.as_deref().unwrap_or_default(),
|
||||||
"targetDocumentId",
|
"targetDocumentId",
|
||||||
)?;
|
)?;
|
||||||
execute_retired_mutation_by_name(
|
reject_cloud_mutation(&context, "")
|
||||||
state.config(),
|
|
||||||
&context,
|
|
||||||
"mediaAssets:patchById",
|
|
||||||
json!({
|
|
||||||
"userId": user_id,
|
|
||||||
"id": asset_id,
|
|
||||||
"patch": {
|
|
||||||
"document_id": target_document_id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
workspace_id.as_deref(),
|
|
||||||
None,
|
|
||||||
"media_batch_move_patch",
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
updated += 1;
|
updated += 1;
|
||||||
@@ -718,14 +628,14 @@ pub async fn media_purge(
|
|||||||
"assetId": asset_id,
|
"assetId": asset_id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
&context,
|
&context,
|
||||||
workspace_id.as_deref(),
|
workspace_id.as_deref(),
|
||||||
command,
|
command,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(ok_response(&context, execution.result))
|
Ok(ok_response(&context, execution))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn media_empty_trash(
|
pub async fn media_empty_trash(
|
||||||
@@ -735,19 +645,7 @@ pub async fn media_empty_trash(
|
|||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
let user_id = current_user_id(&state, &context).await?;
|
let user_id = current_user_id(&state, &context).await?;
|
||||||
let workspace_id = require_id(&context, &body.workspace_id, "workspaceId")?;
|
let workspace_id = require_id(&context, &body.workspace_id, "workspaceId")?;
|
||||||
let result = execute_retired_mutation_by_name(
|
let result = reject_cloud_mutation(&context, "")
|
||||||
state.config(),
|
|
||||||
&context,
|
|
||||||
"mediaAssets:emptyTrashByWorkspace",
|
|
||||||
json!({
|
|
||||||
"userId": user_id,
|
|
||||||
"workspaceId": workspace_id,
|
|
||||||
"expiredDeletedAt": force_expired_at(),
|
|
||||||
}),
|
|
||||||
Some(workspace_id),
|
|
||||||
None,
|
|
||||||
"media_empty_trash",
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
let _ =
|
let _ =
|
||||||
match record_media_empty_trash_artifacts(&state, &context, &user_id, workspace_id, &result)
|
match record_media_empty_trash_artifacts(&state, &context, &user_id, workspace_id, &result)
|
||||||
@@ -818,7 +716,7 @@ pub async fn mindmap_delete(
|
|||||||
"mindmapId": mindmap_id,
|
"mindmapId": mindmap_id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
&context,
|
&context,
|
||||||
workspace_id.as_deref(),
|
workspace_id.as_deref(),
|
||||||
@@ -827,7 +725,7 @@ pub async fn mindmap_delete(
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(ok_response(
|
Ok(ok_response(
|
||||||
&context,
|
&context,
|
||||||
annotate_resource_lifecycle_result(execution.result, "tree.resource.archive", "mindmap"),
|
annotate_resource_lifecycle_result(execution, "tree.resource.archive", "mindmap"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -903,7 +801,7 @@ pub async fn mindmap_trash_action(
|
|||||||
"mindmapId": mindmap_id,
|
"mindmapId": mindmap_id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
&context,
|
&context,
|
||||||
workspace_id.as_deref(),
|
workspace_id.as_deref(),
|
||||||
@@ -912,7 +810,7 @@ pub async fn mindmap_trash_action(
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(ok_response(
|
Ok(ok_response(
|
||||||
&context,
|
&context,
|
||||||
annotate_resource_lifecycle_result(execution.result, command_name, "mindmap"),
|
annotate_resource_lifecycle_result(execution, command_name, "mindmap"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -922,17 +820,7 @@ pub async fn mindmap_empty_trash(
|
|||||||
Json(body): Json<WorkspaceTrashRequest>,
|
Json(body): Json<WorkspaceTrashRequest>,
|
||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
let workspace_id = require_id(&context, &body.workspace_id, "workspaceId")?;
|
let workspace_id = require_id(&context, &body.workspace_id, "workspaceId")?;
|
||||||
let result = execute_retired_mutation_by_name(
|
let result = reject_cloud_mutation(&context, "")
|
||||||
state.config(),
|
|
||||||
&context,
|
|
||||||
"mindmaps:emptyTrashByWorkspace",
|
|
||||||
json!({
|
|
||||||
"workspaceId": workspace_id,
|
|
||||||
}),
|
|
||||||
Some(workspace_id),
|
|
||||||
None,
|
|
||||||
"mindmap_empty_trash",
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
let _ = record_resource_resync_artifacts(
|
let _ = record_resource_resync_artifacts(
|
||||||
&state,
|
&state,
|
||||||
@@ -986,22 +874,7 @@ pub async fn table_create(
|
|||||||
.with_context(&context)
|
.with_context(&context)
|
||||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||||
})?;
|
})?;
|
||||||
let result = execute_retired_mutation_by_name(
|
let result = reject_cloud_mutation(&context, "")
|
||||||
state.config(),
|
|
||||||
&context,
|
|
||||||
"tables:create",
|
|
||||||
json!({
|
|
||||||
"userId": user_id,
|
|
||||||
"workspaceId": workspace_id,
|
|
||||||
"documentId": document_id,
|
|
||||||
"title": body.title,
|
|
||||||
"schema": body.schema.unwrap_or_else(|| json!({})),
|
|
||||||
"snapshot": body.snapshot,
|
|
||||||
}),
|
|
||||||
Some(&workspace_id),
|
|
||||||
None,
|
|
||||||
"table_create",
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
let table_id = result
|
let table_id = result
|
||||||
.get("id")
|
.get("id")
|
||||||
@@ -1093,7 +966,7 @@ async fn table_action(
|
|||||||
"tableId": table_id,
|
"tableId": table_id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
let execution = reject_non_local_runtime_command_with_artifacts(
|
||||||
&state,
|
&state,
|
||||||
&context,
|
&context,
|
||||||
workspace_id.as_deref(),
|
workspace_id.as_deref(),
|
||||||
@@ -1103,7 +976,7 @@ async fn table_action(
|
|||||||
let _ = error_phase;
|
let _ = error_phase;
|
||||||
Ok(ok_response(
|
Ok(ok_response(
|
||||||
&context,
|
&context,
|
||||||
annotate_resource_lifecycle_result(execution.result, command_name, "table"),
|
annotate_resource_lifecycle_result(execution, command_name, "table"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1114,19 +987,7 @@ pub async fn table_empty_trash(
|
|||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
let user_id = current_user_id(&state, &context).await?;
|
let user_id = current_user_id(&state, &context).await?;
|
||||||
let workspace_id = require_id(&context, &body.workspace_id, "workspaceId")?;
|
let workspace_id = require_id(&context, &body.workspace_id, "workspaceId")?;
|
||||||
let result = execute_retired_mutation_by_name(
|
let result = reject_cloud_mutation(&context, "")
|
||||||
state.config(),
|
|
||||||
&context,
|
|
||||||
"tables:emptyTrashByWorkspace",
|
|
||||||
json!({
|
|
||||||
"userId": user_id,
|
|
||||||
"workspaceId": workspace_id,
|
|
||||||
"expiredDeletedAt": force_expired_at(),
|
|
||||||
}),
|
|
||||||
Some(workspace_id),
|
|
||||||
None,
|
|
||||||
"table_empty_trash",
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
let _ = record_resource_resync_artifacts(
|
let _ = record_resource_resync_artifacts(
|
||||||
&state,
|
&state,
|
||||||
@@ -1165,42 +1026,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"documents:getMeta": {"id": "doc_1", "workspace_id": "ws_demo"},
|
|
||||||
"mediaAssets:listByIds": [{"id": "asset_1", "workspace_id": "ws_demo", "document_id": "doc_1"}],
|
|
||||||
"tables:get": {"id": "table_1", "workspace_id": "ws_demo", "document_id": "doc_1"}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
mutation_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"mediaAssets:patchById": {"ok": true},
|
|
||||||
"mediaAssets:purgeById": {"ok": true, "deleted": 1},
|
|
||||||
"mediaAssets:emptyTrashByWorkspace": {"ok": true, "deleted": 2},
|
|
||||||
"bridgeLogs:recordCommandLog": {"ok": true},
|
|
||||||
"bridgeLogs:recordDomainEvent": {"ok": true},
|
|
||||||
"mindmaps:softDelete": {"ok": true, "moved": 1, "deleted_at": "2026-05-15T00:00:00Z"},
|
|
||||||
"mindmaps:restore": {"ok": true, "updated_at": "2026-05-15T00:00:00Z"},
|
|
||||||
"mindmaps:purge": {"ok": true},
|
|
||||||
"mindmaps:emptyTrashByWorkspace": {"ok": true, "deletedCount": 3},
|
|
||||||
"tables:create": {"id": "table_1", "workspace_id": "ws_demo", "document_id": "doc_1", "title": "表格"},
|
|
||||||
"tables:remove": {"success": true, "deleted_at": "2026-05-15T00:00:00Z"},
|
|
||||||
"tables:restore": {"success": true},
|
|
||||||
"tables:purge": {"success": true},
|
|
||||||
"tables:emptyTrashByWorkspace": {"ok": true, "deleted": 4}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -1489,7 +1319,6 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(moved["result"]["moved"], 1);
|
assert_eq!(moved["result"]["moved"], 1);
|
||||||
assert_eq!(moved["result"]["canonicalCommand"], "tree.resource.move");
|
assert_eq!(moved["result"]["canonicalCommand"], "tree.resource.move");
|
||||||
assert_ne!(moved["result"]["canonicalCommand"], "documents.move");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::context::RequestContext;
|
|||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::routes::gateway::current_actor_id;
|
use crate::routes::gateway::current_actor_id;
|
||||||
use crate::routes::query_support::{
|
use crate::routes::query_support::{
|
||||||
execute_runtime_query_against_data, execute_runtime_query_via_legacy_cloud,
|
execute_runtime_query_against_data, reject_non_local_runtime_query,
|
||||||
resolve_effective_workspace_id,
|
resolve_effective_workspace_id,
|
||||||
};
|
};
|
||||||
use crate::routes::web_shell::{escape_script_json, load_sidebar_tree_html};
|
use crate::routes::web_shell::{escape_script_json, load_sidebar_tree_html};
|
||||||
@@ -628,7 +628,7 @@ async fn load_search_results_with_filters(
|
|||||||
"customRangeTo": filters.custom_range.as_ref().and_then(|range| range.to.clone()),
|
"customRangeTo": filters.custom_range.as_ref().and_then(|range| range.to.clone()),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
match execute_runtime_query_via_legacy_cloud(
|
match reject_non_local_runtime_query(
|
||||||
config,
|
config,
|
||||||
context,
|
context,
|
||||||
Some(workspace_id),
|
Some(workspace_id),
|
||||||
@@ -768,17 +768,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -893,23 +887,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn search_documents_does_not_return_builtin_fixture_when_convex_unavailable() {
|
async fn search_documents_does_not_return_builtin_fixture_when_local_required() {
|
||||||
let app = build_app(AppState::new(AppConfig {
|
let app = build_app(AppState::new(AppConfig {
|
||||||
service_name: "mnote-web".into(),
|
service_name: "mnote-web".into(),
|
||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: false,
|
allow_dev_fixtures: false,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -1511,17 +1499,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use control_plane::session_token_hash;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||||
const COOKIE_CONVEX_AUTH_JWT: &str = "__convexAuthJWT";
|
|
||||||
const COOKIE_MNOTE_SESSION: &str = "mnote_session";
|
const COOKIE_MNOTE_SESSION: &str = "mnote_session";
|
||||||
const COOKIE_ACTOR_EMAIL: &str = "mnote_actor_email";
|
const COOKIE_ACTOR_EMAIL: &str = "mnote_actor_email";
|
||||||
const COOKIE_ACTOR_NAME: &str = "mnote_actor_name";
|
const COOKIE_ACTOR_NAME: &str = "mnote_actor_name";
|
||||||
@@ -22,7 +21,6 @@ pub struct RuntimeConfigResponse {
|
|||||||
pub public_entry: String,
|
pub public_entry: String,
|
||||||
pub tree_renderer_family: &'static str,
|
pub tree_renderer_family: &'static str,
|
||||||
pub document_editor_host: &'static str,
|
pub document_editor_host: &'static str,
|
||||||
pub legacy_next_compat_enabled: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -46,7 +44,6 @@ pub async fn runtime_config(State(state): State<AppState>) -> Response {
|
|||||||
public_entry: state.config().public_bind_addr.clone(),
|
public_entry: state.config().public_bind_addr.clone(),
|
||||||
tree_renderer_family: "rust_family",
|
tree_renderer_family: "rust_family",
|
||||||
document_editor_host: "leptos_tiptap_island",
|
document_editor_host: "leptos_tiptap_island",
|
||||||
legacy_next_compat_enabled: state.config().enable_legacy_next_compat,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,23 +159,9 @@ fn effective_actor_type_for_user(user_id: &str, stored_role: &str) -> String {
|
|||||||
/// 仅用于 **展示** email/name 补全;**绝不**作为 user_id / 鉴权真源。
|
/// 仅用于 **展示** email/name 补全;**绝不**作为 user_id / 鉴权真源。
|
||||||
/// 无签名校验:任意客户端可伪造 JWT payload 中的展示字段。
|
/// 无签名校验:任意客户端可伪造 JWT payload 中的展示字段。
|
||||||
/// 身份仍以 control-plane session / header actor 为准(见上方 get_session 分支)。
|
/// 身份仍以 control-plane session / header actor 为准(见上方 get_session 分支)。
|
||||||
fn jwt_cookie_claim(context: &RequestContext, keys: &[&str]) -> Option<String> {
|
fn jwt_cookie_claim(_context: &RequestContext, _keys: &[&str]) -> Option<String> {
|
||||||
let token = context
|
// 身份仅 control-plane session / header;不再从第三方 JWT cookie 补全。
|
||||||
.auth
|
None
|
||||||
.cookie_header
|
|
||||||
.as_deref()
|
|
||||||
.and_then(|cookies| raw_cookie_value(cookies, COOKIE_CONVEX_AUTH_JWT))?;
|
|
||||||
let payload_segment = token.split('.').nth(1)?;
|
|
||||||
let decoded = URL_SAFE_NO_PAD.decode(payload_segment.as_bytes()).ok()?;
|
|
||||||
let payload: serde_json::Value = serde_json::from_slice(&decoded).ok()?;
|
|
||||||
keys.iter().find_map(|key| {
|
|
||||||
payload
|
|
||||||
.get(key)
|
|
||||||
.and_then(serde_json::Value::as_str)
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encoded_cookie_value(cookie_header: &str, name: &str) -> Option<String> {
|
fn encoded_cookie_value(cookie_header: &str, name: &str) -> Option<String> {
|
||||||
@@ -235,17 +218,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -280,7 +257,8 @@ mod tests {
|
|||||||
assert_eq!(payload["owner"], "mnote-web");
|
assert_eq!(payload["owner"], "mnote-web");
|
||||||
assert_eq!(payload["treeRendererFamily"], "rust_family");
|
assert_eq!(payload["treeRendererFamily"], "rust_family");
|
||||||
assert!(payload.get("legacyNextBaseUrl").is_none());
|
assert!(payload.get("legacyNextBaseUrl").is_none());
|
||||||
assert!(payload.get("convexAdminKey").is_none());
|
assert!(payload.get("adminKey").is_none());
|
||||||
|
assert!(payload.get("cloudAdminKey").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -302,7 +280,8 @@ mod tests {
|
|||||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||||
assert_eq!(payload["owner"], "mnote-web");
|
assert_eq!(payload["owner"], "mnote-web");
|
||||||
assert_eq!(payload["userId"], "dev-user");
|
assert_eq!(payload["userId"], "dev-user");
|
||||||
assert!(payload.get("convexAdminKey").is_none());
|
assert!(payload.get("adminKey").is_none());
|
||||||
|
assert!(payload.get("cloudAdminKey").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -328,7 +307,8 @@ mod tests {
|
|||||||
assert_eq!(payload["userId"], "user_real");
|
assert_eq!(payload["userId"], "user_real");
|
||||||
assert_eq!(payload["actorType"], "user");
|
assert_eq!(payload["actorType"], "user");
|
||||||
assert_eq!(payload["authMode"], "forwardedActor");
|
assert_eq!(payload["authMode"], "forwardedActor");
|
||||||
assert!(payload.get("convexAdminKey").is_none());
|
assert!(payload.get("adminKey").is_none());
|
||||||
|
assert!(payload.get("cloudAdminKey").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -338,17 +318,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -408,17 +382,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -489,17 +457,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -575,7 +537,6 @@ mod tests {
|
|||||||
assert_eq!(payload["userId"], "user_real");
|
assert_eq!(payload["userId"], "user_real");
|
||||||
assert_eq!(payload["actorType"], "user");
|
assert_eq!(payload["actorType"], "user");
|
||||||
assert_eq!(payload["authMode"], "forwardedActor");
|
assert_eq!(payload["authMode"], "forwardedActor");
|
||||||
assert_ne!(payload["code"], "legacy_next_compat_disabled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -597,7 +558,6 @@ mod tests {
|
|||||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||||
assert_eq!(payload["owner"], "mnote-web");
|
assert_eq!(payload["owner"], "mnote-web");
|
||||||
assert_eq!(payload["userId"], "dev-user");
|
assert_eq!(payload["userId"], "dev-user");
|
||||||
assert_ne!(payload["code"], "legacy_next_compat_disabled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -619,6 +579,5 @@ mod tests {
|
|||||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||||
assert_eq!(payload["owner"], "mnote-web");
|
assert_eq!(payload["owner"], "mnote-web");
|
||||||
assert_eq!(payload["userId"], "dev-user");
|
assert_eq!(payload["userId"], "dev-user");
|
||||||
assert_ne!(payload["code"], "legacy_next_compat_disabled");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::app::AppConfig;
|
|||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::routes::query_support::{
|
use crate::routes::query_support::{
|
||||||
execute_runtime_query_against_data, fetch_query_data_via_legacy_cloud,
|
execute_runtime_query_against_data, reject_non_local_query_data,
|
||||||
};
|
};
|
||||||
use bridge_runtime::RuntimeQueryEnvelopeWire;
|
use bridge_runtime::RuntimeQueryEnvelopeWire;
|
||||||
use core_protocol::{KernelNodeType, KernelProjectionKind};
|
use core_protocol::{KernelNodeType, KernelProjectionKind};
|
||||||
@@ -38,7 +38,7 @@ pub async fn load_sidebar_dataset(
|
|||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
) -> Result<Value, WebError> {
|
) -> Result<Value, WebError> {
|
||||||
fetch_query_data_via_legacy_cloud(
|
reject_non_local_query_data(
|
||||||
config,
|
config,
|
||||||
context,
|
context,
|
||||||
Some(workspace_id),
|
Some(workspace_id),
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ fn delta_matches_workspace(delta: &Value, subscription_workspace: Option<&str>)
|
|||||||
|
|
||||||
fn live_poll_query(query: &StreamSnapshotQuery) -> StreamSnapshotQuery {
|
fn live_poll_query(query: &StreamSnapshotQuery) -> StreamSnapshotQuery {
|
||||||
let mut next = query.clone();
|
let mut next = query.clone();
|
||||||
// Convex bridgeLogs 的 cursor 是“向更旧记录翻页”,不是 live tail 的起点;
|
// bridgeLogs 的 cursor 是“向更旧记录翻页”,不是 live tail 的起点;
|
||||||
// 实时轮询必须始终查最新窗口,再用 current_cursor 在 Rust 侧比较增量。
|
// 实时轮询必须始终查最新窗口,再用 current_cursor 在 Rust 侧比较增量。
|
||||||
next.cursor = None;
|
next.cursor = None;
|
||||||
next
|
next
|
||||||
@@ -414,17 +414,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(r#"{"sidebar:datasetList":{"active_workspace_id":"ws_demo","documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"trashed_documents":[],"media_assets":[],"trashed_media_assets":[],"mindmap_assets":[],"trashed_mindmap_assets":[],"table_assets":[],"trashed_table_assets":[],"mindmap_docs":[],"mindmap_asset_children":{}},"bridgeLogs:listWorkspaceOverview":{"workspace_id":"ws_demo","command_logs":[{"command_id":"cmd_1","request_id":"req_1","status":"applied","created_at":"2026-04-16T00:00:00Z"}],"domain_events":[{"command_id":"cmd_1","status":"published","created_at":"2026-04-16T00:00:00Z"}],"next_cursor":"cursor_demo","has_more":false,"filters":{"command_status":null,"event_status":null,"target_page_id":null,"target_block_id":null,"aggregate_type":null,"aggregate_id":null},"generated_at":"2026-04-16T00:00:00Z"}}"#.into()),
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::app::AppConfig;
|
|||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::routes::query_support::{
|
use crate::routes::query_support::{
|
||||||
execute_runtime_query_via_legacy_cloud, resolve_effective_workspace_id,
|
reject_non_local_runtime_query, resolve_effective_workspace_id,
|
||||||
};
|
};
|
||||||
use crate::routes::snapshot_support::{
|
use crate::routes::snapshot_support::{
|
||||||
execute_kernel_query, load_projection_snapshot, load_sidebar_dataset, subtree_query,
|
execute_kernel_query, load_projection_snapshot, load_sidebar_dataset, subtree_query,
|
||||||
@@ -635,7 +635,7 @@ pub async fn load_stream_overview(
|
|||||||
.with_context(context)
|
.with_context(context)
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
let overview = execute_runtime_query_via_legacy_cloud(
|
let overview = reject_non_local_runtime_query(
|
||||||
config,
|
config,
|
||||||
context,
|
context,
|
||||||
Some(&effective_workspace_id),
|
Some(&effective_workspace_id),
|
||||||
@@ -690,7 +690,7 @@ pub async fn load_stream_snapshot(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let overview = execute_runtime_query_via_legacy_cloud(
|
let overview = reject_non_local_runtime_query(
|
||||||
config,
|
config,
|
||||||
context,
|
context,
|
||||||
Some(&effective_workspace_id),
|
Some(&effective_workspace_id),
|
||||||
@@ -821,7 +821,7 @@ mod tests {
|
|||||||
"id": "clog_2",
|
"id": "clog_2",
|
||||||
"command_id": "cmd_2",
|
"command_id": "cmd_2",
|
||||||
"created_at": "2026-04-25T10:00:02Z",
|
"created_at": "2026-04-25T10:00:02Z",
|
||||||
"command_name": "documents.emptyTrashByWorkspace",
|
"command_name": "tree.trash.emptyWorkspace",
|
||||||
"payload": {
|
"payload": {
|
||||||
"streamDelta": {
|
"streamDelta": {
|
||||||
"op": "resync_required",
|
"op": "resync_required",
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ use crate::app::AppState;
|
|||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::routes::command_support::{
|
use crate::routes::command_support::{
|
||||||
build_runtime_command_plan, build_tree_target, ensure_non_empty, ensure_sort_order,
|
build_runtime_command_plan, build_tree_target, ensure_non_empty, ensure_sort_order, read_optional_non_empty,
|
||||||
execute_runtime_command_via_legacy_cloud_with_artifacts, read_optional_non_empty,
|
|
||||||
};
|
};
|
||||||
use crate::routes::local_folder_source::{
|
use crate::routes::local_folder_source::{
|
||||||
ensure_local_workspace_access_with_state, ensure_local_workspace_read_access_with_state,
|
ensure_local_workspace_access_with_state, ensure_local_workspace_read_access_with_state,
|
||||||
@@ -12,10 +11,10 @@ use crate::routes::local_folder_source::{
|
|||||||
local_workspace_id_from_root_uri, LocalAccessMode,
|
local_workspace_id_from_root_uri, LocalAccessMode,
|
||||||
};
|
};
|
||||||
use crate::routes::query_support::{
|
use crate::routes::query_support::{
|
||||||
fetch_documents_meta_via_legacy_cloud, resolve_effective_workspace_id,
|
reject_non_local_documents_meta, resolve_effective_workspace_id,
|
||||||
};
|
};
|
||||||
use crate::routes::snapshot_support::{load_projection_snapshot, ProjectionSnapshotSpec};
|
use crate::routes::snapshot_support::{load_projection_snapshot, ProjectionSnapshotSpec};
|
||||||
use crate::transport::legacy_cloud_guard::execute_retired_mutation_by_name;
|
use crate::transport::local_only::local_required;
|
||||||
use crate::tree_shell::filetree_renderer::{
|
use crate::tree_shell::filetree_renderer::{
|
||||||
render_initial_filetree_html, FileTreeInitialRenderInput, FileTreeRenderRow,
|
render_initial_filetree_html, FileTreeInitialRenderInput, FileTreeRenderRow,
|
||||||
};
|
};
|
||||||
@@ -137,69 +136,6 @@ impl TreeCommandEnvelopeContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct TreeCompatAliasCatalogEntry {
|
|
||||||
pub compat_command: &'static str,
|
|
||||||
pub preferred_command: &'static str,
|
|
||||||
pub source_kind: &'static str,
|
|
||||||
pub retained_for: &'static str,
|
|
||||||
pub retirement_condition: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub const TREE_DOCUMENT_COMPAT_ALIAS_CATALOG: &[TreeCompatAliasCatalogEntry] = &[
|
|
||||||
TreeCompatAliasCatalogEntry {
|
|
||||||
compat_command: "documents.create",
|
|
||||||
preferred_command: "tree.node.create",
|
|
||||||
source_kind: "convex_workspace",
|
|
||||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
|
||||||
retirement_condition: "all cloud and remote callers emit tree.node.create",
|
|
||||||
},
|
|
||||||
TreeCompatAliasCatalogEntry {
|
|
||||||
compat_command: "documents.title.update",
|
|
||||||
preferred_command: "tree.node.rename",
|
|
||||||
source_kind: "convex_workspace",
|
|
||||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
|
||||||
retirement_condition: "all cloud and remote callers emit tree.node.rename",
|
|
||||||
},
|
|
||||||
TreeCompatAliasCatalogEntry {
|
|
||||||
compat_command: "documents.move",
|
|
||||||
preferred_command: "tree.subtree.move",
|
|
||||||
source_kind: "convex_workspace",
|
|
||||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
|
||||||
retirement_condition: "all cloud and remote callers emit tree.subtree.move",
|
|
||||||
},
|
|
||||||
TreeCompatAliasCatalogEntry {
|
|
||||||
compat_command: "documents.delete",
|
|
||||||
preferred_command: "tree.node.archive",
|
|
||||||
source_kind: "convex_workspace",
|
|
||||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
|
||||||
retirement_condition: "all cloud and remote callers emit tree.node.archive",
|
|
||||||
},
|
|
||||||
TreeCompatAliasCatalogEntry {
|
|
||||||
compat_command: "documents.restore",
|
|
||||||
preferred_command: "tree.node.restore",
|
|
||||||
source_kind: "convex_workspace",
|
|
||||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
|
||||||
retirement_condition: "all cloud and remote callers emit tree.node.restore",
|
|
||||||
},
|
|
||||||
TreeCompatAliasCatalogEntry {
|
|
||||||
compat_command: "documents.purge",
|
|
||||||
preferred_command: "tree.node.purge",
|
|
||||||
source_kind: "convex_workspace",
|
|
||||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
|
||||||
retirement_condition: "all cloud and remote callers emit tree.node.purge",
|
|
||||||
},
|
|
||||||
TreeCompatAliasCatalogEntry {
|
|
||||||
compat_command: "documents.copy_tree",
|
|
||||||
preferred_command: "tree.subtree.copy",
|
|
||||||
source_kind: "convex_workspace",
|
|
||||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
|
||||||
retirement_condition: "all cloud and remote callers emit tree.subtree.copy",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum TreeCommandRequest {
|
pub enum TreeCommandRequest {
|
||||||
Create {
|
Create {
|
||||||
@@ -323,10 +259,9 @@ fn normalize_bool_flag(value: Option<&str>, default: bool) -> bool {
|
|||||||
|
|
||||||
fn normalize_tree_source_kind(raw: Option<&str>) -> &'static str {
|
fn normalize_tree_source_kind(raw: Option<&str>) -> &'static str {
|
||||||
match raw.map(str::trim).unwrap_or("") {
|
match raw.map(str::trim).unwrap_or("") {
|
||||||
"local_folder" | "local-folder" | "local" => "local_folder",
|
"local_folder" | "local-folder" | "local" | "" => "local_folder",
|
||||||
"convex_workspace" | "convex" | "" => "convex_workspace",
|
// 未知 kind 不再回落到云源;统一 local_folder(调用方仍须提供 rootUri)
|
||||||
// 未知 kind 一律回落到 convex_workspace,避免客户端自报 kind 与 root_uri 脱钩
|
_ => "local_folder",
|
||||||
_ => "convex_workspace",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,39 +270,24 @@ fn build_workspace_source_wire(
|
|||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
envelope_context: &TreeCommandEnvelopeContext,
|
envelope_context: &TreeCommandEnvelopeContext,
|
||||||
) -> bridge_runtime::RuntimeSourceWire {
|
) -> bridge_runtime::RuntimeSourceWire {
|
||||||
// 不信任请求体自报的任意 source_kind:白名单归一化后再与 root_uri 对齐。
|
// 仅 local_folder;root_uri 必须由上游鉴权提供,禁止伪造云 URI。
|
||||||
let source_kind = normalize_tree_source_kind(envelope_context.source_kind.as_deref()).to_string();
|
let source_kind = normalize_tree_source_kind(envelope_context.source_kind.as_deref()).to_string();
|
||||||
let root_uri = match source_kind.as_str() {
|
let root_uri = envelope_context
|
||||||
"local_folder" => {
|
.root_uri
|
||||||
// local_folder 必须带已由上游鉴权的 root_uri;无则不回落到 convex 假 URI
|
.as_ref()
|
||||||
envelope_context
|
.map(|u| u.trim().to_string())
|
||||||
.root_uri
|
.filter(|u| !u.is_empty() && !u.contains(".."));
|
||||||
.as_ref()
|
let capabilities = if envelope_context.source_capabilities.is_empty() {
|
||||||
.map(|u| u.trim().to_string())
|
vec![
|
||||||
.filter(|u| !u.is_empty() && !u.contains(".."))
|
"load-snapshot".into(),
|
||||||
}
|
"watch".into(),
|
||||||
_ => Some(
|
"preflight-command".into(),
|
||||||
envelope_context
|
"execute-command".into(),
|
||||||
.root_uri
|
"resolve-page-aggregate".into(),
|
||||||
.clone()
|
]
|
||||||
.filter(|u| {
|
} else {
|
||||||
let t = u.trim();
|
envelope_context.source_capabilities.clone()
|
||||||
!t.is_empty() && (t.starts_with("convex://") || t.starts_with("workspace:"))
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| format!("convex://workspace/{workspace_id}")),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
let capabilities =
|
|
||||||
if envelope_context.source_capabilities.is_empty() && source_kind == "convex_workspace" {
|
|
||||||
vec![
|
|
||||||
"load-snapshot".into(),
|
|
||||||
"preflight-command".into(),
|
|
||||||
"execute-command".into(),
|
|
||||||
"resolve-page-aggregate".into(),
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
envelope_context.source_capabilities.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
bridge_runtime::RuntimeSourceWire {
|
bridge_runtime::RuntimeSourceWire {
|
||||||
channel: context.source.channel.clone(),
|
channel: context.source.channel.clone(),
|
||||||
@@ -955,7 +875,7 @@ fn build_tree_shell_html(
|
|||||||
let source_kind = projection
|
let source_kind = projection
|
||||||
.get("sourceKind")
|
.get("sourceKind")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.unwrap_or("convex_workspace");
|
.unwrap_or("local_folder");
|
||||||
let root_uri = projection
|
let root_uri = projection
|
||||||
.get("rootUri")
|
.get("rootUri")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
@@ -1990,7 +1910,7 @@ fn create_command_wire(
|
|||||||
}
|
}
|
||||||
TreeCommandRequest::CreateFolder { .. } => Err(WebError::bad_request_code(
|
TreeCommandRequest::CreateFolder { .. } => Err(WebError::bad_request_code(
|
||||||
"tree_command_validation",
|
"tree_command_validation",
|
||||||
"convex_workspace 暂不支持 folder create capability",
|
"仅 local_folder 支持 folder create capability",
|
||||||
)
|
)
|
||||||
.with_context(context)
|
.with_context(context)
|
||||||
.with_header("x-error-phase", "tree_command_validate")),
|
.with_header("x-error-phase", "tree_command_validate")),
|
||||||
@@ -2194,7 +2114,7 @@ fn create_command_wire(
|
|||||||
}
|
}
|
||||||
TreeCommandRequest::DropFiles { .. } => Err(WebError::bad_request_code(
|
TreeCommandRequest::DropFiles { .. } => Err(WebError::bad_request_code(
|
||||||
"tree_command_validation",
|
"tree_command_validation",
|
||||||
"convex_workspace 外部文件 drop 需要走上传 preflight / object storage executor",
|
"外部文件 drop 需要走 local-folder 上传 preflight",
|
||||||
)
|
)
|
||||||
.with_context(context)
|
.with_context(context)
|
||||||
.with_header("x-error-phase", "tree_command_validate")),
|
.with_header("x-error-phase", "tree_command_validate")),
|
||||||
@@ -2280,7 +2200,7 @@ async fn resolve_tree_create_workspace_id(
|
|||||||
|
|
||||||
if let Some(parent_id) = parent_id.map(str::trim).filter(|value| !value.is_empty()) {
|
if let Some(parent_id) = parent_id.map(str::trim).filter(|value| !value.is_empty()) {
|
||||||
let parent_meta =
|
let parent_meta =
|
||||||
fetch_documents_meta_via_legacy_cloud(state.config(), context, None, parent_id).await?;
|
reject_non_local_documents_meta(state.config(), context, None, parent_id).await?;
|
||||||
if let Some(workspace_id) = parent_meta
|
if let Some(workspace_id) = parent_meta
|
||||||
.get("workspace_id")
|
.get("workspace_id")
|
||||||
.or_else(|| parent_meta.get("workspaceId"))
|
.or_else(|| parent_meta.get("workspaceId"))
|
||||||
@@ -2292,33 +2212,8 @@ async fn resolve_tree_create_workspace_id(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let bootstrap = execute_retired_mutation_by_name(
|
// 云 workspace bootstrap 已移除;无 local context 时直接要求 local_folder。
|
||||||
state.config(),
|
Err(local_required(context, "tree_command_workspace_required"))
|
||||||
context,
|
|
||||||
"workspaces:ensureDefaultWorkspace",
|
|
||||||
json!({
|
|
||||||
"fallbackName": context.auth.actor_id,
|
|
||||||
"workspaceIdIfCreate": generate_tree_document_id(),
|
|
||||||
}),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
"tree_command_workspace_bootstrap",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
bootstrap
|
|
||||||
.get("activeWorkspaceId")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
WebError::bad_gateway_code(
|
|
||||||
"workspace_bootstrap_bad_response",
|
|
||||||
"workspaces.ensureDefaultWorkspace 未返回 activeWorkspaceId",
|
|
||||||
)
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", "tree_command_workspace_bootstrap")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn operation_resource_relative_path(value: &Value, key: &str) -> Option<String> {
|
fn operation_resource_relative_path(value: &Value, key: &str) -> Option<String> {
|
||||||
@@ -2505,7 +2400,7 @@ pub async fn tree_command(
|
|||||||
.with_header("x-error-phase", "tree_command_validate"));
|
.with_header("x-error-phase", "tree_command_validate"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let requested_workspace_id = match &request {
|
let _requested_workspace_id = match &request {
|
||||||
TreeCommandRequest::Create { workspace_id, .. } => workspace_id.as_deref(),
|
TreeCommandRequest::Create { workspace_id, .. } => workspace_id.as_deref(),
|
||||||
TreeCommandRequest::CreateFolder { workspace_id, .. } => workspace_id.as_deref(),
|
TreeCommandRequest::CreateFolder { workspace_id, .. } => workspace_id.as_deref(),
|
||||||
TreeCommandRequest::Rename { workspace_id, .. } => workspace_id.as_deref(),
|
TreeCommandRequest::Rename { workspace_id, .. } => workspace_id.as_deref(),
|
||||||
@@ -2659,77 +2554,13 @@ pub async fn tree_command(
|
|||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let effective_workspace_id = match &request {
|
// 非 local_folder 命令一律拒绝
|
||||||
TreeCommandRequest::Create { parent_id, .. } => {
|
return Err(WebError::bad_request_code(
|
||||||
resolve_tree_create_workspace_id(
|
"local_required",
|
||||||
&state,
|
"tree command 仅支持 local_folder(sourceKind=local_folder + rootUri)",
|
||||||
&context,
|
|
||||||
requested_workspace_id,
|
|
||||||
parent_id.as_deref(),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
TreeCommandRequest::CreateFolder { .. } => {
|
|
||||||
return Err(WebError::bad_request_code(
|
|
||||||
"tree_command_validation",
|
|
||||||
"convex_workspace 暂不支持 folder create capability",
|
|
||||||
)
|
|
||||||
.with_context(&context)
|
|
||||||
.with_header("x-error-phase", "tree_command_validate"));
|
|
||||||
}
|
|
||||||
_ => resolve_effective_workspace_id(&context, requested_workspace_id, true)?
|
|
||||||
.expect("workspace_required 已确保存在"),
|
|
||||||
};
|
|
||||||
let needs_move_preflight = matches!(&request, TreeCommandRequest::Move { .. });
|
|
||||||
let mut command_wire = create_command_wire(
|
|
||||||
&context,
|
|
||||||
&effective_workspace_id,
|
|
||||||
request,
|
|
||||||
&envelope_context,
|
|
||||||
)?;
|
|
||||||
if needs_move_preflight {
|
|
||||||
command_wire.preflight_data =
|
|
||||||
Some(load_tree_move_preflight_data(&state, &context, &effective_workspace_id).await?);
|
|
||||||
}
|
|
||||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
|
||||||
&state,
|
|
||||||
&context,
|
|
||||||
Some(&effective_workspace_id),
|
|
||||||
command_wire,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.with_context(&context)
|
||||||
let response_document_id = execution
|
.with_header("x-error-phase", "tree_command_local_required"));
|
||||||
.result
|
|
||||||
.get("id")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.unwrap_or(requested_document_id);
|
|
||||||
let artifacts = execution
|
|
||||||
.artifacts
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|artifacts| serde_json::to_value(artifacts).ok())
|
|
||||||
.unwrap_or(Value::Null);
|
|
||||||
let artifact_error = execution
|
|
||||||
.artifact_error
|
|
||||||
.as_ref()
|
|
||||||
.map(|message| Value::String(message.clone()))
|
|
||||||
.unwrap_or(Value::Null);
|
|
||||||
|
|
||||||
Ok(json_response(
|
|
||||||
&context,
|
|
||||||
json!({
|
|
||||||
"workspaceId": effective_workspace_id,
|
|
||||||
"action": action,
|
|
||||||
"documentId": response_document_id,
|
|
||||||
"parentId": requested_parent_id,
|
|
||||||
"title": requested_title,
|
|
||||||
"sortOrder": requested_sort_order,
|
|
||||||
"updatedAt": execution.result.get("updated_at").cloned().unwrap_or(Value::Null),
|
|
||||||
"execution": execution.result,
|
|
||||||
"artifacts": artifacts,
|
|
||||||
"artifactError": artifact_error,
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reduce_tree_shell_runtime(
|
pub async fn reduce_tree_shell_runtime(
|
||||||
@@ -2752,7 +2583,7 @@ mod tests {
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
collect_filetree_render_rows, create_command_wire, TreeCommandEnvelopeContext,
|
collect_filetree_render_rows, create_command_wire, TreeCommandEnvelopeContext,
|
||||||
TreeCommandRequest, TREE_DOCUMENT_COMPAT_ALIAS_CATALOG,
|
TreeCommandRequest,
|
||||||
};
|
};
|
||||||
use crate::app::{build_app, AppConfig, AppState};
|
use crate::app::{build_app, AppConfig, AppState};
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
@@ -2769,17 +2600,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: true,
|
enable_debug_shell_routes: true,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(r#"{"sidebar:datasetList":{"active_workspace_id":"ws_demo","documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"media_assets":[{"id":"asset_file_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"封面.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_child_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"节点图片.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_pdf_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"说明书.pdf","mime_type":"application/pdf","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_book_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"小说.epub","mime_type":"application/epub+zip","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_assets":[{"id":"mind_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"mindmap","file_name":"头脑风暴.json","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"table_assets":[{"id":"table_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"table","file_name":"预算.table","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_asset_children":{"mind_1":["asset_child_1"]}}}"#.into()),
|
|
||||||
mutation_fixtures_json: Some(r#"{"workspaces:ensureDefaultWorkspace":{"workspaces":[{"id":"ws_demo","name":"我的空间","type":"personal","iconUrl":null,"memberCount":1,"isDefault":true}],"activeWorkspaceId":"ws_demo"},"documents:createWithParentReference":{"ok":true,"id":"page_new","updated_at":"2026-04-17T00:00:00Z"},"documents:updateTitle":{"ok":true,"updated_at":"2026-04-17T00:00:00Z"},"documents:move":{"ok":true,"updated_at":"2026-04-17T00:00:00Z"},"documents:purge":{"ok":true,"deletedCount":1},"bridgeLogs:recordCommandLog":{"ok":true,"id":"clog_fixture"},"bridgeLogs:recordDomainEvent":{"ok":true,"id":"evt_fixture"}}"#.into()),
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -2953,8 +2778,6 @@ mod tests {
|
|||||||
.contains("const usedRustInitialPageRenderer = hydrateInitialPageTree();"));
|
.contains("const usedRustInitialPageRenderer = hydrateInitialPageTree();"));
|
||||||
assert!(TREE_SHELL_RUNTIME_JS.contains("applyCreatedDocumentLocally"));
|
assert!(TREE_SHELL_RUNTIME_JS.contains("applyCreatedDocumentLocally"));
|
||||||
assert!(TREE_SHELL_RUNTIME_JS.contains("applyRemovedDocumentLocally"));
|
assert!(TREE_SHELL_RUNTIME_JS.contains("applyRemovedDocumentLocally"));
|
||||||
assert!(TREE_SHELL_RUNTIME_JS
|
|
||||||
.contains("sourceKind === \"convex_workspace\" && applyCreatedDocumentLocally"));
|
|
||||||
assert!(TREE_SHELL_RUNTIME_JS
|
assert!(TREE_SHELL_RUNTIME_JS
|
||||||
.contains("if (!deletableItems.every((item) => !fileTreeRowById.has(item.rowId)))"));
|
.contains("if (!deletableItems.every((item) => !fileTreeRowById.has(item.rowId)))"));
|
||||||
assert!(TREE_SHELL_RUNTIME_JS.contains("application/x-mnote-page-tree-node"));
|
assert!(TREE_SHELL_RUNTIME_JS.contains("application/x-mnote-page-tree-node"));
|
||||||
@@ -3144,17 +2967,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: true,
|
enable_debug_shell_routes: true,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -3890,17 +3707,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: true,
|
enable_debug_shell_routes: true,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -4428,11 +4239,11 @@ mod tests {
|
|||||||
assert_eq!(create_wire.name, "tree.node.create");
|
assert_eq!(create_wire.name, "tree.node.create");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
create_wire.source.source_kind.as_deref(),
|
create_wire.source.source_kind.as_deref(),
|
||||||
Some("convex_workspace")
|
Some("local_folder")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
create_wire.source.root_uri.as_deref(),
|
create_wire.source.root_uri.as_deref(),
|
||||||
Some("convex://workspace/ws_demo")
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(create_wire.source.workspace_id.as_deref(), Some("ws_demo"));
|
assert_eq!(create_wire.source.workspace_id.as_deref(), Some("ws_demo"));
|
||||||
assert!(create_wire
|
assert!(create_wire
|
||||||
@@ -4477,8 +4288,8 @@ mod tests {
|
|||||||
&HeaderMap::new(),
|
&HeaderMap::new(),
|
||||||
);
|
);
|
||||||
let envelope_context = TreeCommandEnvelopeContext {
|
let envelope_context = TreeCommandEnvelopeContext {
|
||||||
source_kind: Some("convex_workspace".into()),
|
source_kind: Some("local_folder".into()),
|
||||||
root_uri: Some("convex://workspace/ws_demo".into()),
|
root_uri: Some("file:///tmp/ws_demo".into()),
|
||||||
source_capabilities: vec![
|
source_capabilities: vec![
|
||||||
"load-snapshot".into(),
|
"load-snapshot".into(),
|
||||||
"preflight-command".into(),
|
"preflight-command".into(),
|
||||||
@@ -4511,11 +4322,11 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rename_wire.source.source_kind.as_deref(),
|
rename_wire.source.source_kind.as_deref(),
|
||||||
Some("convex_workspace")
|
Some("local_folder")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rename_wire.source.root_uri.as_deref(),
|
rename_wire.source.root_uri.as_deref(),
|
||||||
Some("convex://workspace/ws_demo")
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rename_wire.payload["targetNodeId"],
|
rename_wire.payload["targetNodeId"],
|
||||||
@@ -4557,18 +4368,10 @@ mod tests {
|
|||||||
&TreeCommandEnvelopeContext::default(),
|
&TreeCommandEnvelopeContext::default(),
|
||||||
)
|
)
|
||||||
.expect("tree create wire");
|
.expect("tree create wire");
|
||||||
let compat_create_wire = bridge_runtime::RuntimeCommandEnvelopeWire {
|
|
||||||
name: "documents.create".into(),
|
|
||||||
..tree_create_wire.clone()
|
|
||||||
};
|
|
||||||
let tree_create_plan =
|
let tree_create_plan =
|
||||||
build_runtime_command_plan(&context, Some("ws_demo"), tree_create_wire)
|
build_runtime_command_plan(&context, Some("ws_demo"), tree_create_wire)
|
||||||
.expect("tree create plan");
|
.expect("tree create plan");
|
||||||
let compat_create_plan =
|
|
||||||
build_runtime_command_plan(&context, Some("ws_demo"), compat_create_wire)
|
|
||||||
.expect("compat create plan");
|
|
||||||
assert_eq!(tree_create_plan.function_name, "tree.node.create");
|
assert_eq!(tree_create_plan.function_name, "tree.node.create");
|
||||||
assert_eq!(compat_create_plan.function_name, "documents.create");
|
|
||||||
|
|
||||||
let tree_rename_wire = create_command_wire(
|
let tree_rename_wire = create_command_wire(
|
||||||
&context,
|
&context,
|
||||||
@@ -4581,18 +4384,10 @@ mod tests {
|
|||||||
&TreeCommandEnvelopeContext::default(),
|
&TreeCommandEnvelopeContext::default(),
|
||||||
)
|
)
|
||||||
.expect("tree rename wire");
|
.expect("tree rename wire");
|
||||||
let compat_rename_wire = bridge_runtime::RuntimeCommandEnvelopeWire {
|
|
||||||
name: "documents.title.update".into(),
|
|
||||||
..tree_rename_wire.clone()
|
|
||||||
};
|
|
||||||
let tree_rename_plan =
|
let tree_rename_plan =
|
||||||
build_runtime_command_plan(&context, Some("ws_demo"), tree_rename_wire)
|
build_runtime_command_plan(&context, Some("ws_demo"), tree_rename_wire)
|
||||||
.expect("tree rename plan");
|
.expect("tree rename plan");
|
||||||
let compat_rename_plan =
|
|
||||||
build_runtime_command_plan(&context, Some("ws_demo"), compat_rename_wire)
|
|
||||||
.expect("compat rename plan");
|
|
||||||
assert_eq!(tree_rename_plan.function_name, "tree.node.rename");
|
assert_eq!(tree_rename_plan.function_name, "tree.node.rename");
|
||||||
assert_eq!(compat_rename_plan.function_name, "documents.title.update");
|
|
||||||
|
|
||||||
let tree_move_wire = create_command_wire(
|
let tree_move_wire = create_command_wire(
|
||||||
&context,
|
&context,
|
||||||
@@ -4606,44 +4401,8 @@ mod tests {
|
|||||||
&TreeCommandEnvelopeContext::default(),
|
&TreeCommandEnvelopeContext::default(),
|
||||||
)
|
)
|
||||||
.expect("tree move wire");
|
.expect("tree move wire");
|
||||||
let compat_move_wire = bridge_runtime::RuntimeCommandEnvelopeWire {
|
|
||||||
name: "documents.move".into(),
|
|
||||||
..tree_move_wire.clone()
|
|
||||||
};
|
|
||||||
let tree_move_plan = build_runtime_command_plan(&context, Some("ws_demo"), tree_move_wire)
|
let tree_move_plan = build_runtime_command_plan(&context, Some("ws_demo"), tree_move_wire)
|
||||||
.expect("tree move plan");
|
.expect("tree move plan");
|
||||||
let compat_move_plan =
|
|
||||||
build_runtime_command_plan(&context, Some("ws_demo"), compat_move_wire)
|
|
||||||
.expect("compat move plan");
|
|
||||||
assert_eq!(tree_move_plan.function_name, "tree.subtree.move");
|
assert_eq!(tree_move_plan.function_name, "tree.subtree.move");
|
||||||
assert_eq!(compat_move_plan.function_name, "documents.move");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tree_documents_compat_alias_catalog_marks_cloud_retirement_boundary() {
|
|
||||||
let aliases = TREE_DOCUMENT_COMPAT_ALIAS_CATALOG;
|
|
||||||
assert!(aliases
|
|
||||||
.iter()
|
|
||||||
.all(|entry| entry.compat_command.starts_with("documents.")));
|
|
||||||
assert!(aliases
|
|
||||||
.iter()
|
|
||||||
.all(|entry| entry.preferred_command.starts_with("tree.")));
|
|
||||||
assert!(aliases
|
|
||||||
.iter()
|
|
||||||
.all(|entry| entry.source_kind == "convex_workspace"));
|
|
||||||
assert!(aliases
|
|
||||||
.iter()
|
|
||||||
.all(|entry| entry.retained_for.contains("legacy cloud")));
|
|
||||||
assert!(aliases
|
|
||||||
.iter()
|
|
||||||
.all(|entry| entry.retirement_condition.contains("emit tree.")));
|
|
||||||
assert!(aliases.iter().any(|entry| {
|
|
||||||
entry.compat_command == "documents.delete"
|
|
||||||
&& entry.preferred_command == "tree.node.archive"
|
|
||||||
}));
|
|
||||||
assert!(aliases.iter().any(|entry| {
|
|
||||||
entry.compat_command == "documents.copy_tree"
|
|
||||||
&& entry.preferred_command == "tree.subtree.copy"
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ fn resolve_scope(
|
|||||||
root_uri: &str,
|
root_uri: &str,
|
||||||
scope: &str,
|
scope: &str,
|
||||||
) -> Result<TreeViewStateScope, WebError> {
|
) -> Result<TreeViewStateScope, WebError> {
|
||||||
let source_kind = normalize_or_default(source_kind, "convex_workspace");
|
let source_kind = normalize_or_default(source_kind, "local_folder");
|
||||||
let tree_kind = normalize_tree_kind(context, tree_kind)?;
|
let tree_kind = normalize_tree_kind(context, tree_kind)?;
|
||||||
let root_uri = root_uri.trim().to_string();
|
let root_uri = root_uri.trim().to_string();
|
||||||
let scope = normalize_or_default(scope, "root");
|
let scope = normalize_or_default(scope, "root");
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ fn page_preference_scope(
|
|||||||
) -> Result<PagePreferenceScope, WebError> {
|
) -> Result<PagePreferenceScope, WebError> {
|
||||||
let source_kind = source_kind.trim();
|
let source_kind = source_kind.trim();
|
||||||
let source_kind = if source_kind.is_empty() {
|
let source_kind = if source_kind.is_empty() {
|
||||||
"convex_workspace"
|
"local_folder"
|
||||||
} else {
|
} else {
|
||||||
source_kind
|
source_kind
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -87,17 +87,73 @@ fn actor_id(context: &RequestContext) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn require_authenticated(context: &RequestContext) -> Result<String, WebError> {
|
fn require_authenticated(context: &RequestContext) -> Result<String, WebError> {
|
||||||
|
// 7-76:Web PAT(mnpat1)与密码箱分职 — 禁止用笔记 PAT 读密/管箱。
|
||||||
|
// 读密通道:浏览器 session,或 agent vault token(mnv1.*)。
|
||||||
|
if crate::routes::api_access_token::is_pat_auth(context) {
|
||||||
|
return Err(WebError::new(
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"vault_via_web_pat_forbidden",
|
||||||
|
"Web PAT(mnpat1)不能访问密码箱;请使用浏览器会话,或签发 vault token(mnv1)后调用",
|
||||||
|
)
|
||||||
|
.with_context(context));
|
||||||
|
}
|
||||||
let id = actor_id(context);
|
let id = actor_id(context);
|
||||||
if id == "anonymous" {
|
if id == "anonymous" {
|
||||||
return Err(WebError::new(
|
return Err(WebError::new(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
"vault_auth_required",
|
"vault_auth_required",
|
||||||
"密码箱需要登录会话",
|
"密码箱需要登录会话或 vault token(mnv1)",
|
||||||
));
|
)
|
||||||
|
.with_context(context));
|
||||||
}
|
}
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// agent vault token(auth_method=vault_token)时强制 scope;session/extension 不限。
|
||||||
|
fn require_vault_agent_scope(context: &RequestContext, need: &str) -> Result<(), WebError> {
|
||||||
|
if context.auth.auth_method != "vault_token" {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let need = need.trim();
|
||||||
|
let ok = context.auth.scopes.iter().any(|s| {
|
||||||
|
let s = s.trim();
|
||||||
|
s == "*"
|
||||||
|
|| s == need
|
||||||
|
|| s == format!("vault.{need}")
|
||||||
|
|| (need == "list" && (s == "get" || s == "vault.get")) // get 蕴含 list 选型
|
||||||
|
|| (need == "get" && (s == "list" || s == "vault.list")) // list 常可看 L0 详情
|
||||||
|
});
|
||||||
|
if ok {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(WebError::new(
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"vault_scope_denied",
|
||||||
|
format!("vault token 缺少 scope: {need}"),
|
||||||
|
)
|
||||||
|
.with_context(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 Authorization 提取 mnv1 token。
|
||||||
|
pub fn bearer_mnv1(authorization: Option<&str>) -> Option<&str> {
|
||||||
|
let raw = authorization?
|
||||||
|
.strip_prefix("Bearer ")
|
||||||
|
.or_else(|| authorization?.strip_prefix("bearer "))?
|
||||||
|
.trim();
|
||||||
|
if raw.starts_with("mnv1.") {
|
||||||
|
Some(raw)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 校验 mnv1 vault capability token。
|
||||||
|
pub fn verify_agent_vault_token(token: &str) -> Result<mnote_vault_core::VaultTokenClaims, WebError> {
|
||||||
|
let key = mnote_vault_core::load_or_create_hmac_key(&mnote_vault_core::default_hmac_key_path())
|
||||||
|
.map_err(WebError::from)?;
|
||||||
|
mnote_vault_core::verify_token(&key, token).map_err(WebError::from)
|
||||||
|
}
|
||||||
|
|
||||||
fn require_root_uri(root_uri: Option<&str>) -> Result<&str, WebError> {
|
fn require_root_uri(root_uri: Option<&str>) -> Result<&str, WebError> {
|
||||||
root_uri
|
root_uri
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
@@ -529,6 +585,7 @@ pub async fn list(
|
|||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
require_vault_enabled()?;
|
require_vault_enabled()?;
|
||||||
require_authenticated(&context)?;
|
require_authenticated(&context)?;
|
||||||
|
require_vault_agent_scope(&context, "list")?;
|
||||||
let root_uri = require_root_uri(query.root_uri.as_deref())?;
|
let root_uri = require_root_uri(query.root_uri.as_deref())?;
|
||||||
let root = resolve_read_root(&state, &context, root_uri).await?;
|
let root = resolve_read_root(&state, &context, root_uri).await?;
|
||||||
let status = parse_status(query.status.as_deref())?;
|
let status = parse_status(query.status.as_deref())?;
|
||||||
@@ -594,6 +651,7 @@ pub async fn get_item(
|
|||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
require_vault_enabled()?;
|
require_vault_enabled()?;
|
||||||
require_authenticated(&context)?;
|
require_authenticated(&context)?;
|
||||||
|
require_vault_agent_scope(&context, "get")?;
|
||||||
let root_uri = require_root_uri(query.root_uri.as_deref())?;
|
let root_uri = require_root_uri(query.root_uri.as_deref())?;
|
||||||
let root = resolve_read_root(&state, &context, root_uri).await?;
|
let root = resolve_read_root(&state, &context, root_uri).await?;
|
||||||
let record = vault_store::get_credential(&root, &id)?;
|
let record = vault_store::get_credential(&root, &id)?;
|
||||||
@@ -1190,6 +1248,7 @@ pub async fn resolve_item(
|
|||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
require_vault_enabled()?;
|
require_vault_enabled()?;
|
||||||
let actor = require_authenticated(&context)?;
|
let actor = require_authenticated(&context)?;
|
||||||
|
require_vault_agent_scope(&context, "resolve")?;
|
||||||
let map = body.as_object().ok_or_else(|| {
|
let map = body.as_object().ok_or_else(|| {
|
||||||
WebError::bad_request_code("vault_body_invalid", "请求体必须是 JSON 对象")
|
WebError::bad_request_code("vault_body_invalid", "请求体必须是 JSON 对象")
|
||||||
})?;
|
})?;
|
||||||
@@ -1254,6 +1313,7 @@ pub async fn list_ai(
|
|||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
require_vault_enabled()?;
|
require_vault_enabled()?;
|
||||||
require_authenticated(&context)?;
|
require_authenticated(&context)?;
|
||||||
|
require_vault_agent_scope(&context, "list")?;
|
||||||
let status = parse_status(query.status.as_deref())?;
|
let status = parse_status(query.status.as_deref())?;
|
||||||
// 尽力修复旧数据分组(失败不阻断列表)
|
// 尽力修复旧数据分组(失败不阻断列表)
|
||||||
let repair = repair_ai_folder_paths_best_effort();
|
let repair = repair_ai_folder_paths_best_effort();
|
||||||
@@ -1347,6 +1407,7 @@ pub async fn get_ai_item(
|
|||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
require_vault_enabled()?;
|
require_vault_enabled()?;
|
||||||
require_authenticated(&context)?;
|
require_authenticated(&context)?;
|
||||||
|
require_vault_agent_scope(&context, "get")?;
|
||||||
let result = get_ai_vault_item(&id)?;
|
let result = get_ai_vault_item(&id)?;
|
||||||
Ok(ok_response(&context, result))
|
Ok(ok_response(&context, result))
|
||||||
}
|
}
|
||||||
@@ -1359,6 +1420,7 @@ pub async fn resolve_ai_item(
|
|||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
require_vault_enabled()?;
|
require_vault_enabled()?;
|
||||||
let actor = require_authenticated(&context)?;
|
let actor = require_authenticated(&context)?;
|
||||||
|
require_vault_agent_scope(&context, "resolve")?;
|
||||||
let map = body.as_object().ok_or_else(|| {
|
let map = body.as_object().ok_or_else(|| {
|
||||||
WebError::bad_request_code("vault_body_invalid", "请求体必须是 JSON 对象")
|
WebError::bad_request_code("vault_body_invalid", "请求体必须是 JSON 对象")
|
||||||
})?;
|
})?;
|
||||||
@@ -1405,6 +1467,7 @@ pub async fn login_ai_item(
|
|||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
require_vault_enabled()?;
|
require_vault_enabled()?;
|
||||||
let actor = require_authenticated(&context)?;
|
let actor = require_authenticated(&context)?;
|
||||||
|
require_vault_agent_scope(&context, "login")?;
|
||||||
let force = body
|
let force = body
|
||||||
.get("forceRefresh")
|
.get("forceRefresh")
|
||||||
.or_else(|| body.get("force_refresh"))
|
.or_else(|| body.get("force_refresh"))
|
||||||
@@ -1927,6 +1990,15 @@ pub async fn list_local_agent_vault_tokens(
|
|||||||
axum::extract::Query(query): axum::extract::Query<std::collections::HashMap<String, String>>,
|
axum::extract::Query(query): axum::extract::Query<std::collections::HashMap<String, String>>,
|
||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
require_vault_enabled()?;
|
require_vault_enabled()?;
|
||||||
|
// 本机安装列表仅浏览器 session / admin;Web PAT 不走此面
|
||||||
|
if crate::routes::api_access_token::is_pat_auth(&context) {
|
||||||
|
return Err(WebError::new(
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"vault_via_web_pat_forbidden",
|
||||||
|
"Web PAT 不能查看本机 vault 安装列表",
|
||||||
|
)
|
||||||
|
.with_context(&context));
|
||||||
|
}
|
||||||
let actor = crate::routes::gateway::current_actor_id(&state, &context).ok_or_else(|| {
|
let actor = crate::routes::gateway::current_actor_id(&state, &context).ok_or_else(|| {
|
||||||
WebError::new(
|
WebError::new(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
@@ -2081,6 +2153,7 @@ pub async fn put_ai_session(
|
|||||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||||
require_vault_enabled()?;
|
require_vault_enabled()?;
|
||||||
let actor = require_authenticated(&context)?;
|
let actor = require_authenticated(&context)?;
|
||||||
|
require_vault_agent_scope(&context, "session")?;
|
||||||
let cookie = body
|
let cookie = body
|
||||||
.get("cookieHeader")
|
.get("cookieHeader")
|
||||||
.or_else(|| body.get("cookie_header"))
|
.or_else(|| body.get("cookie_header"))
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ use crate::context::RequestContext;
|
|||||||
use crate::document_buffer_store::{self};
|
use crate::document_buffer_store::{self};
|
||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use crate::page_aggregate::PageAggregate;
|
use crate::page_aggregate::PageAggregate;
|
||||||
use crate::routes::documents::{
|
|
||||||
load_document_content_result, load_document_meta_result, DocumentContentQuery,
|
|
||||||
DocumentMetaQuery,
|
|
||||||
};
|
|
||||||
use crate::routes::gateway::default_workspace_name_for_context;
|
use crate::routes::gateway::default_workspace_name_for_context;
|
||||||
use crate::routes::local_folder_source::{
|
use crate::routes::local_folder_source::{
|
||||||
ensure_local_workspace_read_access_with_state, is_local_access_policy_admin_context,
|
ensure_local_workspace_read_access_with_state, is_local_access_policy_admin_context,
|
||||||
@@ -15,7 +11,6 @@ use crate::routes::local_folder_source::{
|
|||||||
load_local_folder_page_tree_scope_snapshot_with_reveal,
|
load_local_folder_page_tree_scope_snapshot_with_reveal,
|
||||||
load_local_folder_page_tree_snapshot_with_reveal, resolve_local_markdown_page_aggregate,
|
load_local_folder_page_tree_snapshot_with_reveal, resolve_local_markdown_page_aggregate,
|
||||||
};
|
};
|
||||||
use crate::routes::query_support::execute_runtime_query_against_data;
|
|
||||||
use crate::routes::snapshot_support::{
|
use crate::routes::snapshot_support::{
|
||||||
execute_kernel_query, load_projection_snapshot, projection_query, ProjectionSnapshotSpec,
|
execute_kernel_query, load_projection_snapshot, projection_query, ProjectionSnapshotSpec,
|
||||||
};
|
};
|
||||||
@@ -34,7 +29,6 @@ use axum::extract::{Extension, Path, Query, State};
|
|||||||
use axum::http::{header, HeaderMap, HeaderName, HeaderValue, StatusCode, Uri};
|
use axum::http::{header, HeaderMap, HeaderName, HeaderValue, StatusCode, Uri};
|
||||||
use axum::response::{Html, IntoResponse, Response};
|
use axum::response::{Html, IntoResponse, Response};
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use bridge_runtime::RuntimeQueryEnvelopeWire;
|
|
||||||
use control_plane::UpsertNavigationRecentInput;
|
use control_plane::UpsertNavigationRecentInput;
|
||||||
use core_protocol::KernelProjectionKind;
|
use core_protocol::KernelProjectionKind;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -2108,7 +2102,7 @@ pub async fn office_preview_vendor_asset(
|
|||||||
.join(relative);
|
.join(relative);
|
||||||
let bytes = std::fs::read(&path).map_err(|error| {
|
let bytes = std::fs::read(&path).map_err(|error| {
|
||||||
WebError::new(
|
WebError::new(
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::BAD_REQUEST,
|
||||||
"office_preview_vendor_asset_unavailable",
|
"office_preview_vendor_asset_unavailable",
|
||||||
format!("Office preview vendor asset 不可用,请先运行 npm install: {error}"),
|
format!("Office preview vendor asset 不可用,请先运行 npm install: {error}"),
|
||||||
)
|
)
|
||||||
@@ -2391,7 +2385,7 @@ pub async fn pdfjs_asset(Path(asset_path): Path<String>) -> Result<Response, Web
|
|||||||
.join(relative);
|
.join(relative);
|
||||||
let bytes = std::fs::read(&path).map_err(|error| {
|
let bytes = std::fs::read(&path).map_err(|error| {
|
||||||
WebError::new(
|
WebError::new(
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::BAD_REQUEST,
|
||||||
"pdfjs_asset_unavailable",
|
"pdfjs_asset_unavailable",
|
||||||
format!("PDF.js asset 不可用,请先运行 npm install: {error}"),
|
format!("PDF.js asset 不可用,请先运行 npm install: {error}"),
|
||||||
)
|
)
|
||||||
@@ -3084,80 +3078,51 @@ pub(crate) async fn build_page_aggregate_snapshot(
|
|||||||
state: &AppState,
|
state: &AppState,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
document_id: &str,
|
document_id: &str,
|
||||||
workspace_id: Option<&str>,
|
_workspace_id: Option<&str>,
|
||||||
source_kind: Option<&str>,
|
source_kind: Option<&str>,
|
||||||
root_uri: Option<&str>,
|
root_uri: Option<&str>,
|
||||||
) -> Result<PageAggregate, WebError> {
|
) -> Result<PageAggregate, WebError> {
|
||||||
if source_kind.map(str::trim).filter(|value| !value.is_empty()) == Some("local_folder") {
|
let kind = source_kind.map(str::trim).filter(|value| !value.is_empty());
|
||||||
let root_uri = root_uri
|
// local-md: 文档在未显式传 sourceKind 时仍走 local_folder
|
||||||
.map(str::trim)
|
let is_local = kind == Some("local_folder")
|
||||||
.filter(|value| !value.is_empty())
|
|| (kind.is_none() && document_id.trim().starts_with("local-md:"));
|
||||||
.ok_or_else(|| {
|
if !is_local {
|
||||||
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
|
return Err(WebError::bad_request_code(
|
||||||
})?;
|
"local_required",
|
||||||
ensure_local_workspace_read_access_with_state(state, context, root_uri)
|
"Page Aggregate 仅支持 local_folder(sourceKind=local_folder + rootUri)",
|
||||||
.map_err(|error| error.with_context(context))?;
|
)
|
||||||
let root_uri_for_build = root_uri.to_string();
|
.with_context(context));
|
||||||
let root_uri_for_preferences = root_uri_for_build.clone();
|
|
||||||
let document_id = document_id.to_string();
|
|
||||||
let mut aggregate = tokio::task::spawn_blocking(move || {
|
|
||||||
#[cfg(test)]
|
|
||||||
block_local_page_aggregate_for_test();
|
|
||||||
resolve_local_markdown_page_aggregate(&root_uri_for_build, &document_id)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|error| {
|
|
||||||
WebError::internal(format!("本地 Page Aggregate 构建任务失败: {error}"))
|
|
||||||
})??;
|
|
||||||
annotate_local_attachment_refs_authorization(state, context, &mut aggregate)?;
|
|
||||||
super::ui_preferences::apply_effective_page_preferences(
|
|
||||||
state,
|
|
||||||
context,
|
|
||||||
&mut aggregate,
|
|
||||||
source_kind,
|
|
||||||
Some(root_uri_for_preferences.as_str()),
|
|
||||||
)?;
|
|
||||||
return Ok(aggregate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let meta = load_document_meta_result(
|
let root_uri = root_uri
|
||||||
state,
|
.map(str::trim)
|
||||||
context,
|
.filter(|value| !value.is_empty())
|
||||||
DocumentMetaQuery {
|
.ok_or_else(|| {
|
||||||
document_id: document_id.to_string(),
|
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
|
||||||
workspace_id: workspace_id.map(str::to_string),
|
})?;
|
||||||
},
|
ensure_local_workspace_read_access_with_state(state, context, root_uri)
|
||||||
)
|
.map_err(|error| error.with_context(context))?;
|
||||||
.await?;
|
let root_uri_for_build = root_uri.to_string();
|
||||||
let content = load_document_content_result(
|
let root_uri_for_preferences = root_uri_for_build.clone();
|
||||||
state,
|
let document_id = document_id.to_string();
|
||||||
context,
|
let mut aggregate = tokio::task::spawn_blocking(move || {
|
||||||
DocumentContentQuery {
|
#[cfg(test)]
|
||||||
document_id: document_id.to_string(),
|
block_local_page_aggregate_for_test();
|
||||||
workspace_id: workspace_id.map(str::to_string),
|
resolve_local_markdown_page_aggregate(&root_uri_for_build, &document_id)
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let projection = execute_runtime_query_against_data(
|
|
||||||
context,
|
|
||||||
workspace_id,
|
|
||||||
RuntimeQueryEnvelopeWire {
|
|
||||||
name: "page.aggregate.get".into(),
|
|
||||||
payload: json!({
|
|
||||||
"documentId": document_id,
|
|
||||||
"workspaceId": workspace_id,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
json!({
|
|
||||||
"meta": meta,
|
|
||||||
"content": content,
|
|
||||||
}),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
serde_json::from_value::<PageAggregate>(projection).map_err(|error| {
|
|
||||||
WebError::internal(format!("Page Aggregate projection 反序列化失败: {error}"))
|
|
||||||
})
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
WebError::internal(format!("本地 Page Aggregate 构建任务失败: {error}"))
|
||||||
|
})??;
|
||||||
|
annotate_local_attachment_refs_authorization(state, context, &mut aggregate)?;
|
||||||
|
super::ui_preferences::apply_effective_page_preferences(
|
||||||
|
state,
|
||||||
|
context,
|
||||||
|
&mut aggregate,
|
||||||
|
Some("local_folder"),
|
||||||
|
Some(root_uri_for_preferences.as_str()),
|
||||||
|
)?;
|
||||||
|
Ok(aggregate)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn annotate_local_attachment_refs_authorization(
|
fn annotate_local_attachment_refs_authorization(
|
||||||
@@ -3611,50 +3576,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"documents:getMeta": {
|
|
||||||
"id": "doc_1",
|
|
||||||
"workspace_id": "ws_demo",
|
|
||||||
"title": "服务端页面",
|
|
||||||
"updated_at": "2026-04-18T09:30:00Z",
|
|
||||||
"can_edit": true,
|
|
||||||
"word_count": 42,
|
|
||||||
"character_count": 128,
|
|
||||||
"block_count": 3
|
|
||||||
},
|
|
||||||
"documents:getContent": {
|
|
||||||
"content": [{"id": "block_1", "type": "paragraph", "content": []}],
|
|
||||||
"editorDocument": {
|
|
||||||
"documentId": "doc_1",
|
|
||||||
"rootBlockIds": ["editor_1"],
|
|
||||||
"blocks": [{
|
|
||||||
"blockId": "editor_1",
|
|
||||||
"blockType": "paragraph",
|
|
||||||
"contentNodes": [{
|
|
||||||
"payload": {"type": "text", "text": "来自 editorDocument 的正文"},
|
|
||||||
"attrs": {}
|
|
||||||
}],
|
|
||||||
"childBlockIds": []
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"revision": 7,
|
|
||||||
"conflict_detection_key": "doc_1:7",
|
|
||||||
"pageSubtree": {"rootNodeId": "doc_1", "outline": []}
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -3669,37 +3595,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: Some(
|
|
||||||
r#"{
|
|
||||||
"documents:getMeta": {
|
|
||||||
"id": "doc_1",
|
|
||||||
"workspace_id": "ws_demo",
|
|
||||||
"title": "服务端页面",
|
|
||||||
"updated_at": "2026-04-18T09:30:00Z",
|
|
||||||
"can_edit": true,
|
|
||||||
"word_count": 42,
|
|
||||||
"character_count": 128,
|
|
||||||
"block_count": 3
|
|
||||||
},
|
|
||||||
"documents:getContent": {
|
|
||||||
"content": [{"id": "block_1", "type": "paragraph", "content": []}],
|
|
||||||
"revision": 7,
|
|
||||||
"conflict_detection_key": "doc_1:7",
|
|
||||||
"pageSubtree": {"rootNodeId": "doc_1", "outline": []}
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -3785,23 +3685,17 @@ mod tests {
|
|||||||
.expect("grant control-plane read");
|
.expect("grant control-plane read");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app_with_unreachable_convex_without_fixture() -> axum::Router {
|
fn app_without_local_source() -> axum::Router {
|
||||||
build_app(AppState::new(AppConfig {
|
build_app(AppState::new(AppConfig {
|
||||||
service_name: "mnote-web".into(),
|
service_name: "mnote-web".into(),
|
||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: None,
|
|
||||||
enable_legacy_next_compat: false,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: Some("http://127.0.0.1:9".into()),
|
|
||||||
convex_admin_key: Some("test-admin-key".into()),
|
|
||||||
allow_dev_fixtures: false,
|
allow_dev_fixtures: false,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -3891,8 +3785,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn document_shell_local_markdown_without_source_kind_does_not_fall_back_to_convex() {
|
async fn document_shell_local_markdown_without_source_kind_uses_local_inference() {
|
||||||
let response = app_with_unreachable_convex_without_fixture()
|
let response = app_without_local_source()
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.uri("/documents/local-md:docs~2FPlan.md?resourceTab=primary%3A%3Aresource%3Afile%3Afile%3A%2F%2F%2Ftmp%3Adocs%2FPlan.pdf")
|
.uri("/documents/local-md:docs~2FPlan.md?resourceTab=primary%3A%3Aresource%3Afile%3Afile%3A%2F%2F%2Ftmp%3Adocs%2FPlan.pdf")
|
||||||
@@ -3910,7 +3804,7 @@ mod tests {
|
|||||||
.headers()
|
.headers()
|
||||||
.get("x-error-code")
|
.get("x-error-code")
|
||||||
.and_then(|value| value.to_str().ok()),
|
.and_then(|value| value.to_str().ok()),
|
||||||
Some("convex_retired")
|
Some("local_required")
|
||||||
);
|
);
|
||||||
let location = response
|
let location = response
|
||||||
.headers()
|
.headers()
|
||||||
@@ -3934,17 +3828,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -4532,8 +4420,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn page_aggregate_endpoint_errors_without_convex_or_fixture() {
|
async fn page_aggregate_endpoint_errors_without_local_source() {
|
||||||
let response = app_with_unreachable_convex_without_fixture()
|
let response = app_without_local_source()
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.uri("/api/page-aggregate/doc_1?workspaceId=ws_demo")
|
.uri("/api/page-aggregate/doc_1?workspaceId=ws_demo")
|
||||||
@@ -4543,13 +4431,13 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("response");
|
.expect("response");
|
||||||
|
|
||||||
if response.status() != StatusCode::SERVICE_UNAVAILABLE {
|
if response.status() != StatusCode::BAD_REQUEST {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let body = to_bytes(response.into_body(), usize::MAX)
|
let body = to_bytes(response.into_body(), usize::MAX)
|
||||||
.await
|
.await
|
||||||
.expect("body");
|
.expect("body");
|
||||||
panic!(
|
panic!(
|
||||||
"expected SERVICE_UNAVAILABLE, got {status}: {}",
|
"expected BAD_REQUEST, got {status}: {}",
|
||||||
String::from_utf8_lossy(&body)
|
String::from_utf8_lossy(&body)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4558,35 +4446,23 @@ mod tests {
|
|||||||
.headers()
|
.headers()
|
||||||
.get("x-error-code")
|
.get("x-error-code")
|
||||||
.and_then(|value| value.to_str().ok()),
|
.and_then(|value| value.to_str().ok()),
|
||||||
Some("convex_retired")
|
Some("local_required")
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
response
|
|
||||||
.headers()
|
|
||||||
.get("x-error-phase")
|
|
||||||
.and_then(|value| value.to_str().ok()),
|
|
||||||
Some("convex_query_retired")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
response
|
|
||||||
.headers()
|
|
||||||
.get("x-upstream-service")
|
|
||||||
.and_then(|value| value.to_str().ok()),
|
|
||||||
Some("legacy-cloud-retired")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
let body = to_bytes(response.into_body(), usize::MAX)
|
let body = to_bytes(response.into_body(), usize::MAX)
|
||||||
.await
|
.await
|
||||||
.expect("body");
|
.expect("body");
|
||||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||||
assert_eq!(payload["ok"], false);
|
assert_eq!(payload["ok"], false);
|
||||||
assert_eq!(payload["code"], "convex_retired");
|
assert_eq!(payload["code"], "local_required");
|
||||||
assert!(payload.get("schema").is_none());
|
assert!(payload.get("schema").is_none());
|
||||||
assert!(payload.get("result").is_none());
|
assert!(payload.get("result").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn document_shell_errors_without_convex_or_fixture() {
|
async fn document_shell_errors_without_local_source() {
|
||||||
let response = app_with_unreachable_convex_without_fixture()
|
let response = app_without_local_source()
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.uri("/documents/doc_1?workspaceId=ws_demo")
|
.uri("/documents/doc_1?workspaceId=ws_demo")
|
||||||
@@ -4598,13 +4474,13 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("response");
|
.expect("response");
|
||||||
|
|
||||||
if response.status() != StatusCode::SERVICE_UNAVAILABLE {
|
if response.status() != StatusCode::BAD_REQUEST {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let body = to_bytes(response.into_body(), usize::MAX)
|
let body = to_bytes(response.into_body(), usize::MAX)
|
||||||
.await
|
.await
|
||||||
.expect("body");
|
.expect("body");
|
||||||
panic!(
|
panic!(
|
||||||
"expected SERVICE_UNAVAILABLE, got {status}: {}",
|
"expected BAD_REQUEST, got {status}: {}",
|
||||||
String::from_utf8_lossy(&body)
|
String::from_utf8_lossy(&body)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4613,7 +4489,7 @@ mod tests {
|
|||||||
.headers()
|
.headers()
|
||||||
.get("x-error-code")
|
.get("x-error-code")
|
||||||
.and_then(|value| value.to_str().ok()),
|
.and_then(|value| value.to_str().ok()),
|
||||||
Some("convex_retired")
|
Some("local_required")
|
||||||
);
|
);
|
||||||
let body = to_bytes(response.into_body(), usize::MAX)
|
let body = to_bytes(response.into_body(), usize::MAX)
|
||||||
.await
|
.await
|
||||||
@@ -4621,7 +4497,7 @@ mod tests {
|
|||||||
let text = String::from_utf8(body.to_vec()).expect("utf8");
|
let text = String::from_utf8(body.to_vec()).expect("utf8");
|
||||||
let payload: Value = serde_json::from_str(&text).expect("json");
|
let payload: Value = serde_json::from_str(&text).expect("json");
|
||||||
assert_eq!(payload["ok"], false);
|
assert_eq!(payload["ok"], false);
|
||||||
assert_eq!(payload["code"], "convex_retired");
|
assert_eq!(payload["code"], "local_required");
|
||||||
assert!(!text.contains("mnote.page_aggregate.v1"));
|
assert!(!text.contains("mnote.page_aggregate.v1"));
|
||||||
assert!(!text.contains("data-mnote-dev-fixture"));
|
assert!(!text.contains("data-mnote-dev-fixture"));
|
||||||
assert!(!text.contains("data-page-aggregate-snapshot"));
|
assert!(!text.contains("data-page-aggregate-snapshot"));
|
||||||
@@ -4703,17 +4579,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -4772,17 +4642,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -4931,17 +4795,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: true,
|
allow_dev_fixtures: true,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
@@ -4995,7 +4853,7 @@ mod tests {
|
|||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"workspaceId": "ws_demo",
|
"workspaceId": "ws_demo",
|
||||||
"sourceKind": "convex_workspace",
|
"sourceKind": "local_folder",
|
||||||
"documentId": "doc_1",
|
"documentId": "doc_1",
|
||||||
"updates": {
|
"updates": {
|
||||||
"showHeadingNumbers": true,
|
"showHeadingNumbers": true,
|
||||||
@@ -5014,7 +4872,7 @@ mod tests {
|
|||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method("GET")
|
.method("GET")
|
||||||
.uri("/api/ui/preferences/effective?workspaceId=ws_demo&sourceKind=convex_workspace&documentId=doc_1")
|
.uri("/api/ui/preferences/effective?workspaceId=ws_demo&sourceKind=local_folder&documentId=doc_1")
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.expect("request"),
|
.expect("request"),
|
||||||
)
|
)
|
||||||
@@ -5487,17 +5345,11 @@ mod tests {
|
|||||||
service_version: "0.1.0".into(),
|
service_version: "0.1.0".into(),
|
||||||
bind_addr: "127.0.0.1:0".into(),
|
bind_addr: "127.0.0.1:0".into(),
|
||||||
public_bind_addr: "127.0.0.1:3000".into(),
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
|
||||||
enable_legacy_next_compat: true,
|
|
||||||
enable_debug_shell_routes: false,
|
enable_debug_shell_routes: false,
|
||||||
enable_editor_actor: true,
|
enable_editor_actor: true,
|
||||||
enable_page_ai_pi_lab: false,
|
enable_page_ai_pi_lab: false,
|
||||||
compat_next_base_path: "/api/compat/next".into(),
|
|
||||||
convex_url: None,
|
|
||||||
convex_admin_key: None,
|
|
||||||
allow_dev_fixtures: false,
|
allow_dev_fixtures: false,
|
||||||
query_fixtures_json: None,
|
|
||||||
mutation_fixtures_json: None,
|
|
||||||
dev_user_id: "dev-user".into(),
|
dev_user_id: "dev-user".into(),
|
||||||
dev_user_name: "开发用户".into(),
|
dev_user_name: "开发用户".into(),
|
||||||
dev_user_email: "dev@mnote.local".into(),
|
dev_user_email: "dev@mnote.local".into(),
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ mod tests {
|
|||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sidebar-file-tree-root"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sidebar-file-tree-root"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.open"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.open"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.asset.open"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.asset.open"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openConvexAssetFromFileTree"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openLocalAssetFromFileTree"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildMindmapOpenPath"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildMindmapOpenPath"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("isMindmapAssetDetail"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("isMindmapAssetDetail"));
|
||||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("^思维导图"));
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("^思维导图"));
|
||||||
@@ -985,7 +985,7 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
!SIDEBAR_TREE_RUNTIME_JS
|
!SIDEBAR_TREE_RUNTIME_JS
|
||||||
.contains("targetUrl.searchParams.set('sourceKind', 'convex_workspace')"),
|
.contains("targetUrl.searchParams.set('sourceKind', 'convex_workspace')"),
|
||||||
"默认来源菜单不能跳到已退出 dev 主链的 convex_workspace"
|
"默认来源菜单只能走 local_folder,不得设置已退役的云 workspace sourceKind"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,7 +1011,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
TREE_LIVE_CONTROLLER_JS.contains("body.getAttribute('data-mnote-source-kind')"),
|
TREE_LIVE_CONTROLLER_JS.contains("body.getAttribute('data-mnote-source-kind')"),
|
||||||
"根入口由 SSR body 暴露 local_folder 时,tree live 不能误走 retired Convex WS/SSE"
|
"根入口由 SSR body 暴露 local_folder 时,tree live 必须走 local-folder-events,不得走云 WS/SSE"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
TREE_LIVE_CONTROLLER_JS.contains("bootstrap.transport === 'local-folder-events'"),
|
TREE_LIVE_CONTROLLER_JS.contains("bootstrap.transport === 'local-folder-events'"),
|
||||||
@@ -1151,7 +1151,7 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
LOCAL_UPLOAD_RUNTIME_JS
|
LOCAL_UPLOAD_RUNTIME_JS
|
||||||
.contains("var imageUrl = localOpenUrl || fallbackUrl || markdownHref;"),
|
.contains("var imageUrl = localOpenUrl || fallbackUrl || markdownHref;"),
|
||||||
"图片上传首屏必须使用本地 open URL 渲染,避免相对 href 在 /documents 下命中退役 Convex 兼容路径"
|
"图片上传首屏必须使用本地 open URL 渲染,避免相对 href 在 /documents 下命中错误路径"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
LOCAL_UPLOAD_RUNTIME_JS.contains("setImage({ src: imageUrl"),
|
LOCAL_UPLOAD_RUNTIME_JS.contains("setImage({ src: imageUrl"),
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
pub mod legacy_cloud_guard;
|
pub mod local_only;
|
||||||
|
|||||||
@@ -1,274 +0,0 @@
|
|||||||
use crate::app::AppConfig;
|
|
||||||
use crate::context::RequestContext;
|
|
||||||
use crate::error::WebError;
|
|
||||||
use bridge_runtime::{
|
|
||||||
build_runtime_command_artifact_plan, retired_command_transport_function_name,
|
|
||||||
retired_query_transport_function_name, RuntimeCommandArtifactPlan, RuntimeCommandExecutionPlan,
|
|
||||||
RuntimeQueryExecutionPlan,
|
|
||||||
};
|
|
||||||
use serde_json::Value;
|
|
||||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RetiredCloudCommandExecution {
|
|
||||||
pub result: Value,
|
|
||||||
pub artifacts: Option<RuntimeCommandArtifactPlan>,
|
|
||||||
pub artifact_error: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn retired_error(context: &RequestContext, phase: &'static str) -> WebError {
|
|
||||||
WebError::service_unavailable_code(
|
|
||||||
"convex_retired",
|
|
||||||
"旧 cloud/Convex 兼容运行时已退役;请使用 local-first Rust/SQLite control-plane 路径",
|
|
||||||
)
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", phase)
|
|
||||||
.with_header("x-upstream-service", "legacy-cloud-retired")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_query_fixture(
|
|
||||||
config: &AppConfig,
|
|
||||||
context: &RequestContext,
|
|
||||||
plan: &RuntimeQueryExecutionPlan,
|
|
||||||
) -> Result<Option<Value>, WebError> {
|
|
||||||
if !config.allow_dev_fixtures {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(raw) = config.query_fixtures_json.as_deref() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let trimmed = raw.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fixtures: Value = serde_json::from_str(trimmed).map_err(|error| {
|
|
||||||
WebError::internal(format!("MNOTE_WEB_QUERY_FIXTURES_JSON 非法: {error}"))
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", "fixture_parse")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let Some(map) = fixtures.as_object() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
if let Some(fixture) = map.get(plan.function_name.as_str()) {
|
|
||||||
return Ok(Some(fixture.clone()));
|
|
||||||
}
|
|
||||||
Ok(retired_query_transport_function_name(&plan.query_name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|legacy_name| map.get(legacy_name))
|
|
||||||
.cloned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_mutation_fixture_by_name(
|
|
||||||
config: &AppConfig,
|
|
||||||
context: &RequestContext,
|
|
||||||
function_name: &str,
|
|
||||||
) -> Result<Option<Value>, WebError> {
|
|
||||||
if !config.allow_dev_fixtures {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(raw) = config.mutation_fixtures_json.as_deref() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let trimmed = raw.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fixtures: Value = serde_json::from_str(trimmed).map_err(|error| {
|
|
||||||
WebError::internal(format!("MNOTE_WEB_MUTATION_FIXTURES_JSON 非法: {error}"))
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", "fixture_parse")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(fixtures
|
|
||||||
.as_object()
|
|
||||||
.and_then(|map| map.get(function_name))
|
|
||||||
.cloned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_mutation_fixture(
|
|
||||||
config: &AppConfig,
|
|
||||||
context: &RequestContext,
|
|
||||||
plan: &RuntimeCommandExecutionPlan,
|
|
||||||
) -> Result<Option<Value>, WebError> {
|
|
||||||
if let Some(fixture) =
|
|
||||||
load_mutation_fixture_by_name(config, context, plan.function_name.as_str())?
|
|
||||||
{
|
|
||||||
return Ok(Some(fixture));
|
|
||||||
}
|
|
||||||
if let Some(legacy_name) = resource_lifecycle_transport_function_name(plan) {
|
|
||||||
if let Some(fixture) = load_mutation_fixture_by_name(config, context, legacy_name)? {
|
|
||||||
return Ok(Some(fixture));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
retired_command_transport_function_name(&plan.command_name)
|
|
||||||
.ok()
|
|
||||||
.map(|legacy_name| load_mutation_fixture_by_name(config, context, legacy_name))
|
|
||||||
.transpose()
|
|
||||||
.map(|fixture| fixture.flatten())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resource_lifecycle_transport_function_name(
|
|
||||||
plan: &RuntimeCommandExecutionPlan,
|
|
||||||
) -> Option<&'static str> {
|
|
||||||
let action = match plan.command_name.as_str() {
|
|
||||||
"tree.resource.archive" => "archive",
|
|
||||||
"tree.resource.restore" => "restore",
|
|
||||||
"tree.resource.purge" => "purge",
|
|
||||||
"tree.resource.rename" => "rename",
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
let resource_kind = plan
|
|
||||||
.args_json
|
|
||||||
.get("resourceLifecyclePlan")
|
|
||||||
.and_then(|value| value.get("resourceKind"))
|
|
||||||
.or_else(|| plan.args_json.get("resourceKind"))
|
|
||||||
.and_then(Value::as_str)?;
|
|
||||||
match (action, resource_kind) {
|
|
||||||
("archive" | "restore" | "rename", "file" | "media") => Some("mediaAssets:patchById"),
|
|
||||||
("purge", "file" | "media") => Some("mediaAssets:purgeById"),
|
|
||||||
("archive", "mindmap") => Some("mindmaps:softDelete"),
|
|
||||||
("restore", "mindmap") => Some("mindmaps:restore"),
|
|
||||||
("purge", "mindmap") => Some("mindmaps:purge"),
|
|
||||||
("archive", "table") => Some("tables:remove"),
|
|
||||||
("restore", "table") => Some("tables:restore"),
|
|
||||||
("purge", "table") => Some("tables:purge"),
|
|
||||||
("rename", "table") => Some("tables:update"),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute_retired_query_plan(
|
|
||||||
config: &AppConfig,
|
|
||||||
context: &RequestContext,
|
|
||||||
plan: &RuntimeQueryExecutionPlan,
|
|
||||||
) -> Result<Value, WebError> {
|
|
||||||
if plan.function_name.trim().is_empty() || plan.function_name.ends_with(":unknown") {
|
|
||||||
return Err(WebError::bad_request_code(
|
|
||||||
"transport_query_unsupported",
|
|
||||||
format!("mnote-web transport 暂不支持 query: {}", plan.function_name),
|
|
||||||
)
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", "plan_validation"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(fixture) = load_query_fixture(config, context, plan)? {
|
|
||||||
return Ok(fixture);
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(retired_error(context, "convex_query_retired"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute_sidebar_dataset_query(
|
|
||||||
config: &AppConfig,
|
|
||||||
context: &RequestContext,
|
|
||||||
plan: &RuntimeQueryExecutionPlan,
|
|
||||||
) -> Result<Value, WebError> {
|
|
||||||
execute_retired_query_plan(config, context, plan).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute_retired_query_by_name(
|
|
||||||
config: &AppConfig,
|
|
||||||
context: &RequestContext,
|
|
||||||
function_name: &str,
|
|
||||||
args: Value,
|
|
||||||
workspace_id: Option<&str>,
|
|
||||||
error_phase: &'static str,
|
|
||||||
) -> Result<Value, WebError> {
|
|
||||||
let plan = RuntimeQueryExecutionPlan {
|
|
||||||
query_name: function_name.to_string(),
|
|
||||||
function_name: function_name.to_string(),
|
|
||||||
workspace_id: workspace_id.map(ToOwned::to_owned),
|
|
||||||
request_id: context.trace.request_id.clone(),
|
|
||||||
trace_id: context.trace.trace_id.clone(),
|
|
||||||
actor_id: context.auth.actor_id.clone(),
|
|
||||||
payload_json: args.to_string(),
|
|
||||||
args_json: args,
|
|
||||||
};
|
|
||||||
execute_retired_query_plan(config, context, &plan)
|
|
||||||
.await
|
|
||||||
.map_err(|error| error.with_header("x-error-phase", error_phase))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute_retired_command_plan(
|
|
||||||
config: &AppConfig,
|
|
||||||
context: &RequestContext,
|
|
||||||
plan: &RuntimeCommandExecutionPlan,
|
|
||||||
) -> Result<Value, WebError> {
|
|
||||||
if plan.function_name.trim().is_empty() || plan.function_name.ends_with(":unknown") {
|
|
||||||
return Err(WebError::bad_request_code(
|
|
||||||
"transport_command_unsupported",
|
|
||||||
format!(
|
|
||||||
"mnote-web transport 暂不支持 command: {}",
|
|
||||||
plan.function_name
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.with_context(context)
|
|
||||||
.with_header("x-error-phase", "plan_validation"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(fixture) = load_mutation_fixture(config, context, plan)? {
|
|
||||||
return Ok(fixture);
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(retired_error(context, "convex_mutation_retired"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute_retired_mutation_by_name(
|
|
||||||
config: &AppConfig,
|
|
||||||
context: &RequestContext,
|
|
||||||
function_name: &str,
|
|
||||||
args: Value,
|
|
||||||
_workspace_id: Option<&str>,
|
|
||||||
_idempotency_key: Option<&str>,
|
|
||||||
error_phase: &'static str,
|
|
||||||
) -> Result<Value, WebError> {
|
|
||||||
if let Some(fixture) = load_mutation_fixture_by_name(config, context, function_name)? {
|
|
||||||
return Ok(fixture);
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = args;
|
|
||||||
Err(retired_error(context, error_phase))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn persist_runtime_command_artifacts(
|
|
||||||
_config: &AppConfig,
|
|
||||||
_context: &RequestContext,
|
|
||||||
_artifacts: &RuntimeCommandArtifactPlan,
|
|
||||||
) -> Result<(), WebError> {
|
|
||||||
// Convex 已退役。Rust 侧仍会把 artifact plan 返回给调用方和 realtime
|
|
||||||
// consumer;这里保持 no-op,避免兼容路径因为历史持久化层退役而失败。
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute_retired_command_plan_with_artifacts(
|
|
||||||
config: &AppConfig,
|
|
||||||
context: &RequestContext,
|
|
||||||
runtime_context: &bridge_runtime::RuntimeBridgeContextWire,
|
|
||||||
command: &bridge_runtime::RuntimeCommandEnvelopeWire,
|
|
||||||
plan: &RuntimeCommandExecutionPlan,
|
|
||||||
) -> Result<RetiredCloudCommandExecution, WebError> {
|
|
||||||
let result = execute_retired_command_plan(config, context, plan).await?;
|
|
||||||
let now = OffsetDateTime::now_utc()
|
|
||||||
.format(&Rfc3339)
|
|
||||||
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".into());
|
|
||||||
let artifacts =
|
|
||||||
build_runtime_command_artifact_plan(runtime_context, command, plan, &result, &now);
|
|
||||||
let artifact_error = if let Some(artifacts) = artifacts.as_ref() {
|
|
||||||
persist_runtime_command_artifacts(config, context, artifacts)
|
|
||||||
.await
|
|
||||||
.err()
|
|
||||||
.map(|error| error.message().to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Ok(RetiredCloudCommandExecution {
|
|
||||||
result,
|
|
||||||
artifacts,
|
|
||||||
artifact_error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use crate::context::RequestContext;
|
||||||
|
use crate::error::WebError;
|
||||||
|
|
||||||
|
/// local-first only:非 local_folder 请求统一拒绝(无 cloud/兼容执行链)。
|
||||||
|
pub fn local_required(context: &RequestContext, phase: &'static str) -> WebError {
|
||||||
|
WebError::bad_request_code(
|
||||||
|
"local_required",
|
||||||
|
"仅支持 local_folder(sourceKind=local_folder + rootUri)",
|
||||||
|
)
|
||||||
|
.with_context(context)
|
||||||
|
.with_header("x-error-phase", phase)
|
||||||
|
}
|
||||||
@@ -1164,7 +1164,7 @@ fn apply_ok_feedback<E>(
|
|||||||
apply_feedback_result(setter, result.map(|_| ok_message));
|
apply_feedback_result(setter, result.map(|_| ok_message));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_hermes_page_ai_drawer() {
|
fn open_page_ai_drawer() {
|
||||||
let Some(win) = window() else {
|
let Some(win) = window() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1214,7 +1214,7 @@ fn request_ai_edit_bridge(
|
|||||||
set_ai_bridge_message,
|
set_ai_bridge_message,
|
||||||
set_command_feedback,
|
set_command_feedback,
|
||||||
);
|
);
|
||||||
open_hermes_page_ai_drawer();
|
open_page_ai_drawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_layout_density(value: Option<String>) -> String {
|
fn normalize_layout_density(value: Option<String>) -> String {
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ async function seedAiRuntime(requestContext, baseUrl, options) {
|
|||||||
session_id: options.sessionId,
|
session_id: options.sessionId,
|
||||||
run_id: options.runId,
|
run_id: options.runId,
|
||||||
title: options.title,
|
title: options.title,
|
||||||
profile: options.profile || "reasonix",
|
profile: options.profile || "pi",
|
||||||
acp_runtime: options.acpRuntime || options.profile || "reasonix",
|
acp_runtime: options.acpRuntime || options.profile || "pi",
|
||||||
trace_id: options.traceId,
|
trace_id: options.traceId,
|
||||||
status: options.status || "running",
|
status: options.status || "running",
|
||||||
runtime_json: options.runtimeJson || {},
|
runtime_json: options.runtimeJson || {},
|
||||||
|
|||||||
@@ -293,7 +293,6 @@ async function main() {
|
|||||||
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
|
||||||
RUST_LOG: process.env.RUST_LOG || "warn",
|
RUST_LOG: process.env.RUST_LOG || "warn",
|
||||||
};
|
};
|
||||||
const child = spawn(BINARY, [], {
|
const child = spawn(BINARY, [], {
|
||||||
|
|||||||
@@ -64,8 +64,6 @@ function startGateway(port) {
|
|||||||
...buildDocumentFixtureEnv(),
|
...buildDocumentFixtureEnv(),
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
|
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
|
||||||
MNOTE_WEB_LEGACY_NEXT_BASE_URL: "http://127.0.0.1:3100",
|
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "1",
|
|
||||||
};
|
};
|
||||||
return spawn("cargo", ["run", "-q", "-p", "mnote-web", "--bin", "mnote-web"], {
|
return spawn("cargo", ["run", "-q", "-p", "mnote-web", "--bin", "mnote-web"], {
|
||||||
cwd: "/mnt/Data1T/mnote/rust",
|
cwd: "/mnt/Data1T/mnote/rust",
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ function buildFixtureEnv(port) {
|
|||||||
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
|
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
|
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
|
||||||
MNOTE_WEB_LEGACY_NEXT_BASE_URL: "http://127.0.0.1:3100",
|
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "1",
|
|
||||||
MNOTE_WEB_QUERY_FIXTURES_JSON: JSON.stringify({
|
MNOTE_WEB_QUERY_FIXTURES_JSON: JSON.stringify({
|
||||||
"documents:getMeta": {
|
"documents:getMeta": {
|
||||||
id: "doc_auth",
|
id: "doc_auth",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ async function main() {
|
|||||||
assert.equal(recentLocalRoots[0], fileUrl(root), "打开本地文件夹后应记录最近 rootUri");
|
assert.equal(recentLocalRoots[0], fileUrl(root), "打开本地文件夹后应记录最近 rootUri");
|
||||||
|
|
||||||
const bodyText = await page.locator("body").innerText({ timeout: UI_TIMEOUT_MS });
|
const bodyText = await page.locator("body").innerText({ timeout: UI_TIMEOUT_MS });
|
||||||
assert(!bodyText.includes("legacy_next_compat_disabled"), "本地文件夹主入口不能落入 legacy Next fallback");
|
assert(!bodyText.includes("legacy_next_compat"), "本地文件夹主入口不能出现已退役的 legacy Next 文案");
|
||||||
const readmeRow = page.locator('.tree-row[data-row-id="local:markdown:README.md"]');
|
const readmeRow = page.locator('.tree-row[data-row-id="local:markdown:README.md"]');
|
||||||
const docsRow = page.locator('.tree-row[data-row-id="local:folder:docs"]');
|
const docsRow = page.locator('.tree-row[data-row-id="local:folder:docs"]');
|
||||||
await readmeRow.waitFor({
|
await readmeRow.waitFor({
|
||||||
@@ -206,7 +206,7 @@ async function main() {
|
|||||||
await page.goto(BASE_URL, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
await page.goto(BASE_URL, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||||
const finalUrl = new URL(page.url());
|
const finalUrl = new URL(page.url());
|
||||||
assert(
|
assert(
|
||||||
finalUrl.pathname === "/" && !page.url().includes("legacy_next_compat_disabled"),
|
finalUrl.pathname === "/" && !page.url().includes("legacy_next_compat"),
|
||||||
"回到主入口后应仍由 Rust Web 主壳承载",
|
"回到主入口后应仍由 Rust Web 主壳承载",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ function buildFixtureEnv(port) {
|
|||||||
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
|
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
|
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
|
||||||
MNOTE_WEB_LEGACY_NEXT_BASE_URL: "http://127.0.0.1:3100",
|
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "1",
|
|
||||||
MNOTE_WEB_QUERY_FIXTURES_JSON: JSON.stringify({
|
MNOTE_WEB_QUERY_FIXTURES_JSON: JSON.stringify({
|
||||||
"documents:getMeta": {
|
"documents:getMeta": {
|
||||||
id: "doc_1",
|
id: "doc_1",
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ async function main() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
|
||||||
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
||||||
},
|
},
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ async function main() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
|
||||||
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
||||||
},
|
},
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ function startGateway(port) {
|
|||||||
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
|
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
|
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "1",
|
|
||||||
MNOTE_WEB_LEGACY_NEXT_BASE_URL: "http://127.0.0.1:3100",
|
|
||||||
},
|
},
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ async function main() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
|
||||||
...controlPlaneEnv,
|
...controlPlaneEnv,
|
||||||
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ async function main() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
|
||||||
MNOTE_LOCAL_ACCESS_POLICY_FILE: policyFile,
|
MNOTE_LOCAL_ACCESS_POLICY_FILE: policyFile,
|
||||||
},
|
},
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ async function main() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
|
||||||
...controlPlaneEnv,
|
...controlPlaneEnv,
|
||||||
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -161,7 +161,6 @@ async function main() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
|
||||||
...controlPlaneEnv,
|
...controlPlaneEnv,
|
||||||
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ async function main() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
|
||||||
...controlPlaneEnv,
|
...controlPlaneEnv,
|
||||||
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ function startGateway(port, devHot) {
|
|||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
|
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
|
||||||
MNOTE_WEB_LEGACY_NEXT_BASE_URL: "http://127.0.0.1:3100",
|
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "1",
|
|
||||||
};
|
};
|
||||||
delete env.MNOTE_WEB_DEV_HOT_RELOAD;
|
delete env.MNOTE_WEB_DEV_HOT_RELOAD;
|
||||||
if (devHot) env.MNOTE_WEB_DEV_HOT_RELOAD = "1";
|
if (devHot) env.MNOTE_WEB_DEV_HOT_RELOAD = "1";
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ function startGateway(port) {
|
|||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
|
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
|
||||||
MNOTE_WEB_LEGACY_NEXT_BASE_URL: "http://127.0.0.1:3100",
|
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "1",
|
|
||||||
};
|
};
|
||||||
const child = spawn("cargo", ["run", "-q", "-p", "mnote-web", "--bin", "mnote-web"], {
|
const child = spawn("cargo", ["run", "-q", "-p", "mnote-web", "--bin", "mnote-web"], {
|
||||||
cwd: "/mnt/Data1T/mnote/rust",
|
cwd: "/mnt/Data1T/mnote/rust",
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ async function main() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
|
||||||
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
||||||
},
|
},
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ async function startLocalServer(dataRoot) {
|
|||||||
...process.env,
|
...process.env,
|
||||||
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
||||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
|
||||||
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
||||||
},
|
},
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
|||||||
Reference in New Issue
Block a user