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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# 阶段 6 D1:Tauri 壳 + NSIS 安装包,在 windows-latest 上打包并做安装冒烟(docs/roadmap/phase6-desktop.md §2 验收)。
# 沙箱是 aarch64 Linux,Windows 安装包只能在这里打;S 本机不装 Rust / Node,直接下载这里的工件试装。
name: desktop

on:
workflow_dispatch:
pull_request:
paths:
- "desktop/**"
- "packaging/**"
- "app/**"
- "ui/web/**"
- "requirements.txt"
- ".github/workflows/desktop.yml"

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

jobs:
build:
runs-on: windows-latest
timeout-minutes: 60
env:
PYTHONUTF8: "1"
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: "3.11"
cache: pip
cache-dependency-path: |
requirements.txt
packaging/requirements-build.txt
- uses: actions/setup-node@v5
with:
node-version: "24"
cache: npm
cache-dependency-path: ui/web/package-lock.json
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: desktop/src-tauri
- name: 安装 Python 依赖
run: python -m pip install -r requirements.txt -r packaging/requirements-build.txt
- name: 构建前端
working-directory: ui/web
run: |
npm ci
npm run build
- name: 打包 sidecar 并放进壳的 resources
shell: pwsh
run: |
python packaging/build_sidecar.py
Copy-Item -Recurse -Force dist/datachat-sidecar/* desktop/src-tauri/sidecar/
- name: 打包壳(NSIS)
working-directory: desktop
shell: pwsh
run: |
$t = Get-Date
npx --yes @tauri-apps/cli@2 build
"build_seconds=$([int]((Get-Date) - $t).TotalSeconds)" | Tee-Object -FilePath ../desktop-report.txt
$setup = Get-ChildItem src-tauri/target/release/bundle/nsis/*.exe | Select-Object -First 1
"installer=$($setup.Name)" | Tee-Object -FilePath ../desktop-report.txt -Append
"installer_mb=$([math]::Round($setup.Length / 1MB, 1))" | Tee-Object -FilePath ../desktop-report.txt -Append
- name: 安装冒烟(静默安装 → --smoke → 强杀壳 → 静默卸载)
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$rep = "desktop-report.txt"
$setup = (Get-ChildItem desktop/src-tauri/target/release/bundle/nsis/*.exe | Select-Object -First 1).FullName
Start-Process $setup -ArgumentList "/S" -Wait
$dir = Join-Path $env:LOCALAPPDATA "DataChat"
if (-not (Test-Path $dir)) { throw "安装目录不存在:$dir" }
$exe = Get-ChildItem $dir -Filter *.exe | Where-Object { $_.Name -notmatch "uninstall" } | Select-Object -First 1
"installed_exe=$($exe.Name)" | Tee-Object -FilePath $rep -Append
if (-not (Test-Path (Join-Path $dir "sidecar/datachat-sidecar.exe"))) { throw "sidecar 没装上" }

# ① --smoke:起 sidecar → 首页 200 → POST /shutdown → 退出码 0
$p = Start-Process $exe.FullName -ArgumentList "--smoke", "$PWD\smoke-shell.json" -Wait -PassThru
Get-Content smoke-shell.json | Tee-Object -FilePath $rep -Append
if ($p.ExitCode -ne 0) { throw "--smoke 失败,退出码 $($p.ExitCode)" }

# ② 强杀壳,sidecar 必须被 Job Object 一起带走
$hold = Start-Process $exe.FullName -ArgumentList "--smoke-hold", "$PWD\sidecar.pid" -PassThru
for ($i = 0; $i -lt 60 -and -not (Test-Path sidecar.pid); $i++) { Start-Sleep -Milliseconds 500 }
$sp = Get-Content sidecar.pid
if ($sp -notmatch '^\d+$') { throw "smoke-hold 没起来:$sp" }
if (-not (Get-Process -Id $sp -ErrorAction SilentlyContinue)) { throw "sidecar 没在运行" }
Stop-Process -Id $hold.Id -Force
Start-Sleep -Seconds 3
$left = Get-Process -Id $sp -ErrorAction SilentlyContinue
"orphan_after_kill=$([bool]$left)" | Tee-Object -FilePath $rep -Append
if ($left) { throw "壳被强杀后 sidecar(pid $sp)还活着" }

# ③ 静默卸载:安装目录删掉,数据目录保留(卸载页勾选框默认不勾)
$data = Join-Path $env:LOCALAPPDATA "datachat"
New-Item -ItemType Directory -Force $data | Out-Null
Set-Content (Join-Path $data "keep.txt") "x"
Start-Process (Join-Path $dir "uninstall.exe") -ArgumentList "/S" -Wait
for ($i = 0; $i -lt 60 -and (Test-Path (Join-Path $dir $exe.Name)); $i++) { Start-Sleep -Milliseconds 500 }
$gone = -not (Test-Path (Join-Path $dir $exe.Name))
$kept = Test-Path (Join-Path $data "keep.txt")
"uninstalled=$gone data_kept=$kept" | Tee-Object -FilePath $rep -Append
if (-not $gone) { throw "卸载后程序还在" }
if (-not $kept) { throw "卸载把数据目录删了" }
- uses: actions/upload-artifact@v7
if: always()
with:
name: desktop-report
path: |
desktop-report.txt
smoke-shell.json
retention-days: 30
- uses: actions/upload-artifact@v7
with:
name: datachat-setup-win64
path: desktop/src-tauri/target/release/bundle/nsis/*.exe
retention-days: 14
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@
- `app/eval/` —— 评测:50 题用例、比较器(顺序/答案形态规则见 comparator.py 文件头)、报告
- `ui/web/` —— 前端(React + TS),由 `python -m app.server --open` 托管

## 安装(Windows 桌面版,阶段 6)

DataChat 用来**分析你自己的数据**:表里的内容会被模型读到,不要加载来路不明的 CSV(THREAT_MODEL §4 (a))。

1. 从 GitHub Actions 的 `desktop` 工作流下载工件 `datachat-setup-win64`,解压得到 `DataChat_<版本>_x64-setup.exe`。
2. 双击安装(装在当前用户下,不要管理员权限)。安装包没有签名,第一次运行 SmartScreen 会拦:点「更多信息 → 仍要运行」。没有 WebView2 时安装程序会在线下载。
3. 从开始菜单打开 DataChat。第一次会直接进设置页:先填 API Key 并保存,再上传 CSV。
4. 升级:直接装新版本的安装包,数据不会丢。
5. 卸载:「设置 → 应用」里卸载。数据目录 `%LOCALAPPDATA%\datachat` 默认保留(卸载页勾选「删除应用数据」才删);保存在 Windows 凭据管理器里的 Key 不会被删,要删就运行 `python -m app.core.keystore delete`,或在凭据管理器里删掉 datachat 那一项。

桌面版和浏览器模式(`python -m app.server --open`)共用同一个数据目录,两种方式随便切换,但不要同时开。

## 运行

```bash
Expand Down
7 changes: 5 additions & 2 deletions app/server/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@ def main(argv=None):
import webbrowser
url = f"http://127.0.0.1:{port}/#token={app.state.token}"
threading.Timer(1.0, webbrowser.open, [url]).start()
config = uvicorn.Config(app, log_level="info", log_config=None)
uvicorn.Server(config).run(sockets=[sock])
# timeout_graceful_shutdown:SSE 长连接不会自己断,不设的话 should_exit 之后会一直等下去
config = uvicorn.Config(app, log_level="info", log_config=None, timeout_graceful_shutdown=2)
server = uvicorn.Server(config)
app.state.request_shutdown = lambda: setattr(server, "should_exit", True) # POST /shutdown(阶段 6 D1)
server.run(sockets=[sock])


if __name__ == "__main__":
Expand Down
14 changes: 14 additions & 0 deletions app/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

接口(除 /health 和前端静态文件外都要 Authorization: Bearer <token>;图表另可用 ?token=,方便 <img>):
GET /health
POST /shutdown 桌面壳退出前请求后端自行退出(202;未挂退出钩子时 503)
POST /datasets multipart:files(一个或多个 CSV,可含 *.dict.csv 字典)、name
GET /datasets 列表
GET /datasets/{id}?rows=20 详情(文件、每张表的列与概要);带 rows(1–100)时加每张表前 N 行预览,
Expand Down Expand Up @@ -320,6 +321,19 @@ def preview_table(name, filename, df, enc, rows):
def health():
return {"ok": True}

# 阶段 6 D1:桌面壳正常退出时先请求后端自己退(SQLite 收尾、worker 清理),等 3 s 再靠 Job Object 强杀。
# app.state.request_shutdown 由 app.server.__main__ 挂上(uvicorn.Server.should_exit);
# 没挂时(TestClient、被别的程序嵌入)返回 503,不假装成功。
app.state.request_shutdown = None

@app.post("/shutdown", dependencies=[Depends(auth)], status_code=202)
def shutdown():
fn = app.state.request_shutdown
if fn is None:
raise HTTPException(503, "shutdown not available")
fn()
return {"ok": True}

@app.post("/datasets", dependencies=[Depends(auth)])
def create_dataset(files: list[UploadFile] = File(...), name: str = Form(""), db=Depends(db_dep)):
if not files or len(files) > MAX_FILES:
Expand Down
4 changes: 4 additions & 0 deletions desktop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
src-tauri/target/
src-tauri/gen/
src-tauri/sidecar/*
!src-tauri/sidecar/.gitkeep
Loading
Loading