diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
new file mode 100644
index 0000000..c2896b0
--- /dev/null
+++ b/.github/workflows/verify.yml
@@ -0,0 +1,40 @@
+name: Verify
+on:
+ push:
+ pull_request:
+jobs:
+ regression:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: npm
+ - name: Install desktop system libraries
+ run: sudo apt-get update && sudo apt-get install -y libegl1 libopengl0
+ - run: python -m pip install -r requirements-dev.txt
+ - run: npm ci --ignore-scripts
+ - run: PYTHONPATH=engine python -m coverage run -m unittest discover -s tests -v
+ - run: |
+ python -m coverage combine
+ mkdir -p test-results
+ python -m coverage xml -o test-results/coverage.xml
+ - run: QT_QPA_PLATFORM=offscreen python tests/qt_smoke.py
+ - run: npm test
+ - run: python -m ruff check engine sidecar tests
+ - run: python -m compileall -q engine sidecar
+ - run: |
+ for source in renderer/js/*.js electron/*.js tests/*.js tests/*.cjs; do node --check "$source"; done
+ bash -n start.sh stop.sh
+ - run: node node_modules/electron/install.js
+ - run: xvfb-run -a python tests/electron_smoke.py
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: desktop-smoke
+ path: test-results/
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..812b559
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+node_modules/
+.venv/
+__pycache__/
+*.py[cod]
+.ruff_cache/
+data/*
+!data/projects/
+!data/projects/.gitkeep
+dscode_data/
+.DS_Store
+test-results/
+.coverage
+coverage.xml
+.coverage.*
diff --git a/DS code 2.0.1-0712-NEXT demo/README.md b/DS code 2.0.1-0712-NEXT demo/README.md
index 128e21e..62af67e 100644
--- a/DS code 2.0.1-0712-NEXT demo/README.md
+++ b/DS code 2.0.1-0712-NEXT demo/README.md
@@ -1,98 +1,8 @@
-# DS Code v2.0.1-0712-NEXT
-
-> AI 编程助手 · Electron + Python Sidecar · DeepSeek V4 API
-
-## 快速启动 / Quick Start
+此目录仅保留旧启动路径的兼容入口。完整源码、依赖和说明都在仓库根目录:
```bash
-cd "DS code 2.0.1-0712-NEXT demo"
+cd ..
./start.sh
```
-首次启动会自动下载 Electron,之后秒开。
-
-## 目录结构 / File Structure
-
-```
-├── start.sh # 一键启动脚本
-├── stop.sh # 停止脚本
-├── sidecar/ # Python Sidecar (纯 stdlib HTTP/SSE)
-│ ├── sidecar.py # 主程序 (端口 8765)
-│ ├── sidecar_engine.py # 引擎封装 (备用)
-│ └── sidecar_api.py # FastAPI 版本 (备用)
-├── renderer/ # 前端界面
-│ ├── index.html # 主页面
-│ ├── js/
-│ │ ├── app.js # 主逻辑 (向导/聊天/设置)
-│ │ ├── i18n.js # 中英文国际化
-│ │ └── utils.js # Markdown 渲染 / 工具函数
-│ └── css/
-│ ├── theme.css # 设计令牌 + 深色模式
-│ └── app.css # 布局 + 组件样式
-├── engine/src/ # AI 引擎
-│ ├── core/ # ChatEngine / DeepSeek API / Config / Hooks
-│ ├── tools/ # 91 内置工具
-│ ├── sandbox/ # 项目隔离 / 历史记录
-│ ├── prompts/ # 系统提示词
-│ └── agents/ # 子代理
-├── electron/ # Electron 窗口
-├── data/ # 配置和数据
-├── assets/ # 图标/图片
-└── node_modules/ # 依赖 (npm install 后)
-```
-
-## 功能 / Features
-
-### 对话 / Chat
-- DeepSeek V4 Pro / Flash 流式对话
-- Thinking 模式:实时推理链输出
-- 对话历史自动保存与恢复
-
-### 引导页 / Onboarding Wizard
-- 选择服务提供商:DeepSeek 官方 / 自定义供应商
-- 自定义供应商支持:Base URL + 模型配置 (最多 2 个)
-- 1M Context 开关 per model
-
-### 工具调用 / Tool Calls
-- 91 个内置工具 (文件操作 / 命令执行 / Git / 搜索 / MCP / …)
-- 原生 DeepSeek V4 function calling (`tools` 参数)
-- 文本提取 fallback 兼容旧格式
-
-### UI
-- `/` 斜杠命令菜单
-- 中英文双语 (实时切换)
-- 深色/浅色主题
-- 项目侧边栏 (创建/切换/删除)
-- 待办事项、文件管理面板
-
-### API 端点 / API Endpoints
-| 端点 | 说明 |
-|---|---|
-| `GET /api/health` | 健康检查 |
-| `GET /api/config` | 读取配置 |
-| `PUT /api/config` | 更新配置 |
-| `GET /api/projects` | 列出项目 |
-| `POST /api/projects` | 创建项目 |
-| `DELETE /api/projects/{name}` | 删除项目 |
-| `GET /api/chat/stream` | SSE 流式对话 |
-| `POST /api/chat/stop` | 停止对话 |
-| `GET /api/balance` | 查询余额 (http.client) |
-| `GET /api/models` | 列出模型 |
-
-## 配置 / Configuration
-
-首次启动会弹出引导页。配置加密存储在 `~/dscode_data/config.enc` (Fernet)。
-
-## 技术栈 / Tech Stack
-
-| 层 | 技术 |
-|---|---|
-| 前端 | HTML/CSS/JS (vanilla, zero framework) |
-| 后端 | Python 3.9+ asyncio stdlib (zero pip install) |
-| 引擎 | OpenAI SDK + http.client |
-| 流式 | Server-Sent Events (SSE) |
-| 配置 | Fernet 加密存储 |
-
-## License
-
-Apache-2.0
+旧路径 `./start.sh` 会转发到根目录,不再维护重复的前端副本。
diff --git a/DS code 2.0.1-0712-NEXT demo/package.json b/DS code 2.0.1-0712-NEXT demo/package.json
deleted file mode 100644
index acca814..0000000
--- a/DS code 2.0.1-0712-NEXT demo/package.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "name": "dscode-v2",
- "version": "2.0.1-0712-NEXT",
- "description": "DScode V2 — Electron + Python Sidecar",
- "main": "electron/main.js",
- "scripts": {
- "start": "electron .",
- "sidecar": "python3 sidecar/sidecar.py",
- "dev": "concurrently \"npm run sidecar\" \"npm start\""
- },
- "dependencies": {
- "electron": "^33.0.0"
- },
- "devDependencies": {
- "concurrently": "^9.0.0"
- }
-}
diff --git a/DS code 2.0.1-0712-NEXT demo/renderer/css/app.css b/DS code 2.0.1-0712-NEXT demo/renderer/css/app.css
deleted file mode 100644
index 9076e66..0000000
--- a/DS code 2.0.1-0712-NEXT demo/renderer/css/app.css
+++ /dev/null
@@ -1,344 +0,0 @@
-/* DScode V2 — 完整布局 + 组件,圆角卡片风 */
-
-/* ── 侧边栏 ── */
-#sidebar {
- width: 220px; min-width: 220px;
- background: var(--card); border-right: 1px solid var(--border);
- display: flex; flex-direction: column;
- transition: width .2s, min-width .2s; overflow: hidden;
-}
-#sidebar.collapsed { width: 0; min-width: 0; padding: 0; }
-
-#sidebar-expand-btn {
- display: none; position: fixed; left: 6px; top: 14px; z-index: 50;
- width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
- background: var(--card); color: var(--text-3); font-size: 16px; cursor: pointer;
- box-shadow: 0 2px 8px rgba(0,0,0,.08);
-}
-#sidebar-expand-btn:hover { color: var(--text); background: var(--surface); }
-#sidebar.collapsed ~ #sidebar-expand-btn { display: flex; align-items: center; justify-content: center; }
-
-#sidebar-hdr { display: flex; align-items: center; gap: 6px; padding: 14px 12px 8px; }
-#sidebar-ttl { font-weight: 700; font-size: 15px; color: var(--accent); }
-.btn-icon { font-size: 16px; cursor: pointer; color: var(--text-3); user-select: none; padding: 2px; }
-.btn-icon:hover { color: var(--text); }
-
-#btn-new-chat {
- margin: 0 8px 8px; padding: 8px;
- border: none; border-radius: 10px;
- background: var(--accent); color: #fff; font-size: 13px; font-weight: 500; cursor: pointer;
-}
-#btn-new-chat:hover { background: var(--accent-hover); }
-
-#project-list { flex: 1; overflow-y: auto; padding: 0 6px; }
-.pj-item {
- padding: 8px 10px; border-radius: 8px; cursor: pointer;
- font-size: 13px; color: var(--text-2); display: flex; align-items: center; gap: 6px;
-}
-.pj-item:hover { background: var(--surface); }
-.pj-item.sel { background: var(--accent-light); color: var(--accent); font-weight: 500; }
-.pj-item .pj-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
-
-#sidebar-btm {
- padding: 8px; border-top: 1px solid var(--border); display: flex; gap: 6px;
-}
-.btn-btm {
- padding: 7px 8px; background: transparent; color: var(--text-3);
- border: none; border-radius: 8px; cursor: pointer; font-size: 12px; text-align: left;
- white-space: nowrap;
-}
-.btn-btm:hover { background: var(--surface); color: var(--text); }
-
-/* ── 主区域 ── */
-#main-area { flex: 1; display: flex; flex-direction: column; min-width: 0; }
-
-/* ── 聊天 ── */
-#chat-container { flex: 1; overflow-y: auto; padding: 24px 32px 0; }
-#chat-msgs { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; gap: 14px; padding-bottom: 12px; }
-
-/* ── 欢迎卡(圆角大卡片 + 投影)── */
-#welcome-card {
- text-align: center; padding: 32px 0;
- background: var(--card); border-radius: 20px; border: none;
- box-shadow: 0 2px 16px rgba(0,0,0,.06);
- max-width: 420px; margin: 24px auto;
-}
-.wc-icon { width: 64px; height: 64px; object-fit: contain; display: block; margin: 0 auto 8px; }
-#welcome-card h2 { font-size: 20px; font-weight: 700; margin-bottom: 4px; color: var(--text); }
-#welcome-card > p { font-size: 13px; color: var(--text-3); margin-bottom: 20px; }
-
-/* 2x2 统计格子 */
-#wc-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); border-radius: 12px; overflow: hidden; margin: 0 20px; }
-.wc-cell { background: var(--bg); padding: 12px 12px; text-align: center; }
-.wc-t { display: block; font-size: 10px; color: var(--text-4); text-transform: uppercase; margin-bottom: 3px; letter-spacing: 0.3px; }
-.wc-v { display: block; font-size: 13px; font-weight: 600; color: var(--text); }
-
-/* ── 气泡(圆角 iMessage 风)── */
-.bubble { max-width: 85%; word-wrap: break-word; line-height: 1.65; font-size: 14px; }
-.bubble-user {
- align-self: flex-end; background: var(--accent); color: #fff;
- padding: 10px 16px; border-radius: 18px 4px 18px 18px;
-}
-.bubble-ai { align-self: flex-start; }
-.bubble-ai .ai-inner {
- background: var(--card); border: 1px solid var(--border);
- padding: 12px 16px; border-radius: 4px 18px 18px 18px;
- box-shadow: 0 1px 4px rgba(0,0,0,.04);
-}
-.bubble-ai .ai-content { white-space: pre-wrap; }
-.bubble-error .ai-inner { border-left: 3px solid var(--danger); }
-
-/* ── 推理块 ── */
-.rsn-block { margin-bottom: 6px; }
-.rsn-hdr { font-size: 11px; color: var(--text-3); cursor: pointer; padding: 2px 0; user-select: none; display: flex; align-items: center; gap: 4px; }
-.rsn-hdr:hover { color: var(--text); }
-.rsn-body { font-size: 12px; color: var(--reasoning-text); padding: 8px 12px; background: var(--surface); border-radius: 8px; margin-top: 4px; white-space: pre-wrap; line-height: 1.5; max-height: 180px; overflow-y: auto; }
-.rsn-body.hidden { display: none; }
-
-/* ── 工具卡片 ── */
-.tool-card { font-size: 11px; margin: 4px 0; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); overflow: hidden; }
-.tool-hdr { display: flex; align-items: center; gap: 6px; padding: 6px 10px; cursor: pointer; font-family: var(--font-code); color: var(--text-2); }
-.tool-hdr:hover { background: var(--accent-light); }
-.tool-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
-.tool-dot.ok { background: var(--success); }
-.tool-body { padding: 6px 10px; font-size: 11px; color: var(--text-2); white-space: pre-wrap; max-height: 120px; overflow-y: auto; border-top: 1px solid var(--border); display: none; }
-
-/* ── 状态信息卡(圆角胶囊,可点击打开快捷设置)── */
-#status-card {
- display: flex; align-items: center; justify-content: space-between;
- margin: 0 24px 2px; padding: 8px 16px; font-size: 11px; cursor: pointer;
- background: var(--card); border: 1px solid var(--border); border-radius: 14px;
- box-shadow: 0 1px 4px rgba(0,0,0,.04);
-}
-#status-card:hover { border-color: var(--accent); box-shadow: 0 2px 8px rgba(0,0,0,.08); }
-#st-left, #st-right { display: flex; align-items: center; gap: 6px; }
-#st-left { color: var(--text-2); font-weight: 500; }
-#st-right { color: var(--text-3); font-size: 10px; }
-.st-sep { color: var(--border); }
-#st-model { color: var(--accent); font-weight: 600; }
-
-/* ── 输入区 ── */
-#input-area { display: flex; align-items: center; gap: 6px; padding: 8px 16px 6px; position: relative; }
-#input-box {
- flex: 1; resize: none; box-sizing: border-box; border: 2px solid var(--border); border-radius: 24px;
- padding: 10px 16px; font-size: 14px; font-family: var(--font);
- background: var(--card); color: var(--text); outline: none;
- min-height: 40px; max-height: 140px; line-height: 1.45;
- transition: border-color .15s;
-}
-#input-box:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); }
-#btn-attach, #btn-send, #btn-stop {
- width: 38px; height: 38px; border-radius: 50%; border: none; cursor: pointer;
- display: flex; align-items: center; justify-content: center; font-size: 16px;
-}
-#btn-attach { background: transparent; color: var(--text-3); font-weight: 700; }
-#btn-attach:hover { background: var(--surface); color: var(--text); }
-#btn-send { background: var(--accent); color: #fff; }
-#btn-send:disabled { background: var(--border); color: var(--text-4); cursor: default; }
-#btn-send:hover:not(:disabled) { background: var(--accent-hover); }
-#btn-stop { background: var(--danger); color: #fff; }
-.hidden { display: none !important; }
-#disclaimer { text-align: center; padding: 0 16px 8px; font-size: 9px; }
-#disclaimer a { color: var(--text-4); text-decoration: none; }
-
-/* ── 快捷设置弹窗(圆角大卡片)── */
-.qs-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 100; align-items: center; justify-content: center; }
-.qs-overlay.show { display: flex; }
-#qs-card { background: var(--card); padding: 24px 28px; border-radius: 18px; box-shadow: 0 12px 48px rgba(0,0,0,.15); min-width: 320px; max-width: 380px; }
-#qs-card h3 { font-size: 16px; font-weight: 700; margin-bottom: 16px; color: var(--text); }
-.qs-sec { margin-bottom: 14px; }
-.qs-sec label { display: block; font-size: 10px; font-weight: 600; color: var(--text-3); text-transform: uppercase; margin-bottom: 4px; letter-spacing: .3px; }
-.qs-row { display: flex; gap: 5px; }
-.qs-btn {
- flex: 1; padding: 7px 6px; background: var(--surface); color: var(--text-2);
- border: 1px solid var(--border); border-radius: 7px; font-size: 11px; cursor: pointer; text-align: center;
-}
-.qs-btn.on { background: var(--accent); color: #fff; border-color: var(--accent); }
-.qs-btn:hover:not(.on) { background: var(--accent-light); }
-.qs-close {
- margin-top: 14px; width: 100%; padding: 9px; border: 1px solid var(--border);
- border-radius: 8px; font-size: 13px; cursor: pointer; background: transparent; color: var(--text-3);
-}
-.qs-close:hover { background: var(--surface); }
-
-/* ── 右侧面板 ── */
-#right-panel { width: 240px; min-width: 240px; display: flex; flex-direction: column; gap: 8px; padding: 8px; overflow-y: auto; }
-.rp-card { background: var(--card); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; flex-shrink: 0; }
-.rp-card.collapsed .rp-card-body { display: none; }
-.rp-card-hdr { display: flex; align-items: center; justify-content: space-between; padding: 7px 10px; background: var(--surface); border-bottom: 1px solid var(--border); cursor: pointer; user-select: none; }
-.rp-card-hdr:hover { background: var(--accent-light); }
-.rp-card-title { font-size: 10px; font-weight: 600; color: var(--text-2); text-transform: uppercase; letter-spacing: .3px; }
-.rp-card-count { font-size: 9px; color: var(--text-4); background: var(--border); padding: 1px 6px; border-radius: 8px; }
-.rp-chevron { font-size: 10px; color: var(--text-4); transition: transform .15s; }
-.rp-card.collapsed .rp-chevron { transform: rotate(-90deg); }
-.rp-folder-btn { background: transparent; border: none; cursor: pointer; font-size: 13px; padding: 2px 4px; border-radius: 4px; color: var(--text-3); }
-.rp-folder-btn:hover { background: var(--accent-light); color: var(--accent); }
-.rp-empty { font-size: 11px; color: var(--text-4); text-align: center; padding: 16px 10px; }
-
-/* ── 待办 ── */
-.todo-input-row { display: flex; gap: 4px; padding: 6px 8px; border-bottom: 1px solid var(--border); }
-.todo-input-row input { flex: 1; border: none; padding: 5px 6px; font-size: 11px; font-family: var(--font); background: transparent; color: var(--text); outline: none; }
-.todo-input-row button { width: 22px; height: 22px; border-radius: 50%; border: none; background: var(--accent); color: #fff; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
-#todos-list { max-height: 180px; overflow-y: auto; padding: 4px 0; }
-.todo-row { display: flex; align-items: center; gap: 6px; padding: 5px 8px; font-size: 12px; color: var(--text-2); }
-.todo-row:hover { background: var(--surface); }
-.todo-row .todo-dot { width: 14px; height: 14px; border: 1.5px solid var(--border); border-radius: 3px; flex-shrink: 0; cursor: pointer; }
-.todo-row .todo-dot.done { background: var(--accent); border-color: var(--accent); }
-.todo-row .todo-dot.done::after { content: '✓'; color: #fff; font-size: 9px; position: absolute; top: -1px; left: 2px; }
-.todo-row .todo-dot.done { position: relative; }
-.todo-row .todo-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.todo-row .todo-text.done { color: var(--text-4); text-decoration: line-through; }
-.todo-row .todo-del { opacity: 0; cursor: pointer; color: var(--danger); font-size: 14px; padding: 0 2px; }
-.todo-row:hover .todo-del { opacity: 1; }
-
-/* ── 文件 ── */
-#files-body { max-height: 260px; overflow-y: auto; padding: 4px 0; }
-.file-row { display: flex; align-items: center; gap: 5px; padding: 4px 8px; font-size: 11px; color: var(--text-2); cursor: pointer; font-family: var(--font-code); }
-.file-row:hover { background: var(--surface); }
-.file-icon { font-size: 13px; flex-shrink: 0; }
-/* ── 详细设置弹窗 ── */
-#ds-card {
- background: var(--card); padding: 20px 24px; border-radius: 18px; box-shadow: 0 12px 48px rgba(0,0,0,.15);
- min-width: 380px; max-width: 440px; max-height: 80vh; overflow-y: auto;
-}
-#ds-card h3 { font-size: 16px; font-weight: 700; margin-bottom: 12px; color: var(--text); }
-.ds-sec { margin-bottom: 10px; }
-.ds-sec label { display: block; font-size: 10px; font-weight: 600; color: var(--text-3); text-transform: uppercase; margin-bottom: 3px; letter-spacing: .3px; }
-.ds-sec input, .ds-sec select {
- width: 100%; padding: 7px 10px; border: 1px solid var(--border); border-radius: 6px;
- font-size: 12px; font-family: var(--font); background: var(--surface); color: var(--text); outline: none;
-}
-.ds-sec input:focus { border-color: var(--accent); }
-.ds-saved { font-size: 9px; color: var(--success); margin-top: 2px; display: inline-block; transition: opacity .3s; opacity: 0; }
-.ds-tabs { display: flex; gap: 4px; margin-bottom: 14px; }
-.ds-tab {
- flex: 1; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px;
- background: transparent; color: var(--text-3); font-size: 12px; cursor: pointer; text-align: center;
-}
-.ds-tab.active { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 500; }
-.ds-tab-page { margin-bottom: 10px; }
-.ds-about { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
-.ds-about-sub { font-size: 11px; color: var(--text-4); margin-bottom: 4px; }
-.ds-footer { margin-top: 14px; }
-
-/* ── 首次引导向导 ── */
-#wiz-overlay { z-index: 200; }
-#wiz-card {
- background: var(--card); padding: 36px 28px; border-radius: 24px; box-shadow: 0 16px 56px rgba(0,0,0,.18);
- max-width: 400px; text-align: center;
-}
-.wiz-page h2 { font-size: 20px; font-weight: 700; margin: 8px 0 4px; color: var(--text); }
-.wiz-page h3 { font-size: 18px; font-weight: 700; margin: 8px 0 16px; color: var(--text); }
-.wiz-page p { font-size: 13px; color: var(--text-3); margin-bottom: 20px; }
-.wiz-icon { width: 72px; height: 72px; object-fit: contain; display: block; margin: 0 auto 12px; }
-.wiz-icon-sm { width: 48px; height: 48px; object-fit: contain; display: block; margin: 0 auto 8px; }
-.wiz-input {
- width: 100%; padding: 10px 14px; border: 2px solid var(--border); border-radius: 12px;
- font-size: 14px; font-family: var(--font); background: var(--bg); color: var(--text); outline: none; margin-bottom: 16px; text-align: center;
-}
-.wiz-input:focus { border-color: var(--accent); }
-.wiz-next {
- padding: 10px 32px; border: none; border-radius: 24px; background: var(--accent); color: #fff;
- font-size: 14px; font-weight: 600; cursor: pointer;
-}
-.wiz-next:disabled { background: var(--border); color: var(--text-4); cursor: default; }
-.wiz-next:hover:not(:disabled) { background: var(--accent-hover); }
-.wiz-disclaimer-list { text-align: left; margin-bottom: 12px; padding-left: 20px; font-size: 12px; color: var(--text-2); line-height: 1.9; }
-.wiz-link { display: block; margin-bottom: 16px; font-size: 12px; color: var(--accent); text-decoration: underline; cursor: pointer; }
-.wiz-check { display: flex; align-items: center; gap: 6px; justify-content: center; font-size: 12px; color: var(--text-2); margin-bottom: 16px; cursor: pointer; }
-.wiz-check input[type=checkbox] { width: 16px; height: 16px; cursor: pointer; }
-
-/* ── wizard: provider selection ── */
-.wiz-provider-cards { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
-.wiz-prov-card {
- padding: 16px 14px; border: 2px solid var(--border); border-radius: 14px;
- cursor: pointer; text-align: left; transition: border-color .15s, box-shadow .15s;
-}
-.wiz-prov-card:hover { border-color: var(--accent); box-shadow: 0 2px 12px rgba(59,130,246,.12); }
-.wiz-prov-card.selected { border-color: var(--accent); background: var(--accent-light); }
-.wiz-prov-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
-.wiz-prov-icon { font-size: 24px; margin-bottom: 4px; }
-.wiz-prov-name { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 2px; }
-.wiz-prov-hint { font-size: 11px; color: var(--text-3); }
-
-/* ── wizard: custom provider ── */
-.wiz-field { margin-bottom: 16px; }
-.wiz-hint { font-size: 11px; color: var(--text-4); margin-top: 2px; margin-bottom: 0; line-height: 1.4; }
-.wiz-model-card {
- background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
- padding: 14px; margin-bottom: 16px; text-align: left;
-}
-.wiz-model-card-title {
- font-size: 13px; font-weight: 600; color: var(--text-2); margin-bottom: 10px;
- padding-bottom: 8px; border-bottom: 1px solid var(--border);
-}
-.wiz-model-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
-.wiz-model-name {
- flex: 1; padding: 6px 10px; border: 1px solid var(--border); border-radius: 8px;
- font-size: 12px; font-family: var(--font); background: var(--card); color: var(--text);
- outline: none; min-width: 0;
-}
-.wiz-model-name:focus { border-color: var(--accent); }
-.wiz-toggle-group { display: flex; gap: 3px; flex-shrink: 0; }
-.wiz-toggle-group .qs-btn { flex: none; padding: 4px 10px; font-size: 10px; border-radius: 6px; }
-.wiz-model-del-btn {
- width: 24px; height: 24px; border: none; border-radius: 50%;
- background: transparent; color: var(--text-4); cursor: pointer;
- font-size: 12px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
-}
-.wiz-model-del-btn:hover { background: var(--danger); color: #fff; }
-.wiz-add-model-btn {
- display: block; width: 100%; padding: 6px; border: 1px dashed var(--border);
- border-radius: 8px; background: transparent; color: var(--accent); font-size: 12px;
- cursor: pointer; font-family: var(--font);
-}
-.wiz-add-model-btn:hover { background: var(--accent-light); border-style: solid; }
-
-/* ── 流式速度 & token 统计 ── */
-#token-stats {
- font-size: 10px; color: var(--text-4); text-align: right; padding: 2px 0; min-height: 14px;
-}
-
-/* ── AI 操作按钮 ── */
-.ai-actions { display: flex; gap: 8px; margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--border); }
-.ai-actions button {
- padding: 3px 10px; border: none; border-radius: 14px; font-size: 10px; cursor: pointer;
- background: var(--surface); color: var(--text-3);
-}
-.ai-actions button:hover { background: var(--accent-light); color: var(--accent); }
-
-/* ── 斜杠命令菜单 ── */
-#cmd-popup {
- display: none; position: absolute; bottom: 100%; left: 16px; right: 60px;
- background: var(--card); border: 1px solid var(--border); border-radius: 12px;
- box-shadow: 0 8px 32px rgba(0,0,0,.12); padding: 6px; z-index: 50;
- max-height: 240px; overflow-y: auto; margin-bottom: 4px;
-}
-#cmd-popup.show { display: block; }
-.cmd-item {
- display: flex; align-items: center; gap: 8px;
- padding: 8px 10px; border-radius: 8px; cursor: pointer;
- font-size: 13px; color: var(--text-2);
-}
-.cmd-item:hover, .cmd-item.sel { background: var(--accent-light); color: var(--accent); }
-.cmd-item .cmd-icon { font-size: 16px; width: 20px; text-align: center; flex-shrink: 0; }
-.cmd-item .cmd-name { font-weight: 600; min-width: 80px; }
-.cmd-item .cmd-desc { font-size: 11px; color: var(--text-4); flex: 1; }
-
-/* ── Easter Egg ── */
-#ee-overlay {
- display: none; position: fixed; inset: 0; z-index: 999;
- background: rgba(0,0,0,.7); align-items: center; justify-content: center;
-}
-#ee-overlay.show { display: flex; }
-#ee-overlay img {
- max-width: 80vw; max-height: 80vh; border-radius: 20px;
- box-shadow: 0 0 60px rgba(255,255,255,.3);
- animation: eePop .4s cubic-bezier(.34,1.56,.64,1);
-}
-@keyframes eePop {
- from { transform: scale(0); opacity: 0; }
- to { transform: scale(1); opacity: 1; }
-}
-.code-block { background: var(--code-bg); padding: 10px 14px; border-radius: 6px; font-family: var(--font-code); font-size: 12px; line-height: 1.5; overflow-x: auto; margin: 6px 0; white-space: pre; }
-.inline-code { background: var(--code-bg); padding: 1px 4px; border-radius: 3px; font-family: var(--font-code); font-size: 12px; color: var(--accent); }
diff --git a/DS code 2.0.1-0712-NEXT demo/renderer/css/theme.css b/DS code 2.0.1-0712-NEXT demo/renderer/css/theme.css
deleted file mode 100644
index 496a03e..0000000
--- a/DS code 2.0.1-0712-NEXT demo/renderer/css/theme.css
+++ /dev/null
@@ -1,72 +0,0 @@
-/* DScode V2 — 设计令牌 + 深色模式 */
-
-:root {
- --bg: #F8F9FB;
- --card: #FFFFFF;
- --text: #111827;
- --text-2: #374151;
- --text-3: #6B7280;
- --text-4: #9CA3AF;
- --surface: #F0F2F5;
- --accent: #3B82F6;
- --accent-hover: #2563EB;
- --accent-light: #DBEAFE;
- --border: #E5E7EB;
- --divider: #F3F4F6;
- --success: #10B981;
- --danger: #EF4444;
- --code-bg: #F3F4F6;
- --reasoning-text: #6B7280;
- --scrollbar: #D1D5DB;
- --shadow: 0 1px 3px rgba(0,0,0,0.08);
- --radius-sm: 6px;
- --radius-md: 10px;
- --radius-lg: 16px;
- --radius-pill: 20px;
- --font: -apple-system, "SF Pro Display", "SF Pro Text", "PingFang SC", "Microsoft YaHei", sans-serif;
- --font-code: "SF Mono", Menlo, Consolas, monospace;
-}
-
-[data-theme="dark"] {
- --bg: #0F172A;
- --card: #1E293B;
- --text: #F1F5F9;
- --text-2: #CBD5E1;
- --text-3: #94A3B8;
- --text-4: #64748B;
- --surface: #1A2332;
- --accent: #60A5FA;
- --accent-hover: #3B82F6;
- --accent-light: #1E3A5F;
- --border: #334155;
- --divider: #1E293B;
- --success: #34D399;
- --danger: #F87171;
- --code-bg: #1A2332;
- --reasoning-text: #94A3B8;
- --scrollbar: #334155;
- --shadow: 0 1px 3px rgba(0,0,0,0.25);
-}
-
-* { margin: 0; padding: 0; box-sizing: border-box; }
-
-body {
- font-family: var(--font);
- font-size: 14px;
- background: var(--bg);
- color: var(--text);
- overflow: hidden;
- height: 100vh;
- -webkit-font-smoothing: antialiased;
-}
-
-#app {
- display: flex;
- height: 100vh;
-}
-
-/* Scrollbar */
-::-webkit-scrollbar { width: 6px; }
-::-webkit-scrollbar-track { background: transparent; }
-::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 3px; }
-::-webkit-scrollbar-thumb:hover { background: var(--text-4); }
diff --git a/DS code 2.0.1-0712-NEXT demo/renderer/index.html b/DS code 2.0.1-0712-NEXT demo/renderer/index.html
deleted file mode 100644
index 9dfa501..0000000
--- a/DS code 2.0.1-0712-NEXT demo/renderer/index.html
+++ /dev/null
@@ -1,241 +0,0 @@
-
-
-
-
-
-DS Code
-
-
-
-
-
-
-
-
☰
-
-
-
-
-
-
-
DS Code
-
v2.0.1 NEXT — AI that writes code for you
-
-
模型 V4 Pro
-
缓存命中 --
-
本次成本 --
-
账户余额 --
-
-
-
-
-
-
-
- ·
- ·
-
-
-
- ·
- ·
-
-
-
-
-
-
- +
-
- ➤
- ■
-
-
-
-
-
-
-
-
-
-
-
Quick Settings
-
-
-
-
-
Close
-
-
-
-
-
-
-
Settings
-
- Basic
- Provider
- About
-
-
-
-
Nickname
-
API Key
-
Base URL
-
-
-
-
-
-
-
-
-
-
DS Code v2.0.1-0712 NEXT
-
Powered by DeepSeek V4 API · Electron + Python Sidecar
-
Log path: dscode_data/projects/
-
-
- DS Code is an AI programming assistant. Generated content may contain errors — please verify. We are not responsible for any consequences arising from the use of this tool. Do not violate local laws. Use AI responsibly and maintain critical judgment of all output.
-
-
Terms of Service
-
-
-
-
-
-
-
-
-
-
-
-
-
Welcome to DS Code
-
AI coding assistant, always at your service
-
Get Started →
-
-
-
-
Choose Your Provider
-
Select how you want to connect
-
-
-
🛰
-
DeepSeek Official
-
Use the official DeepSeek API service
-
-
-
🔧
-
Custom Provider
-
Connect to your own OpenAI-compatible endpoint
-
-
-
Continue →
-
-
-
-
Custom Provider Setup
-
-
-
*Currently only supports OpenAI-compatible endpoints
-
-
-
Model Info
-
-
+ model
-
-
Continue →
-
-
-
-
Enter your API Key
-
-
Continue →
-
-
-
-
What should we call you
-
-
Continue →
-
-
-
-
Before you start
-
- DS Code is an AI tool and may generate inaccurate content. Please verify.
- We are not responsible for any consequences of using this tool. Do not violate local laws.
- Use AI responsibly. Maintain critical judgment of all output.
-
-
View Terms of Service →
-
I have read and agree to the above
-
Start Using →
-
-
-
-
-
-
-
-
-
-
-
diff --git a/DS code 2.0.1-0712-NEXT demo/renderer/js/app.js b/DS code 2.0.1-0712-NEXT demo/renderer/js/app.js
deleted file mode 100644
index 14bdce3..0000000
--- a/DS code 2.0.1-0712-NEXT demo/renderer/js/app.js
+++ /dev/null
@@ -1,531 +0,0 @@
-// DScode V2 v2.0.1-0712-NEXT
-(function(){'use strict';
-var API='http://127.0.0.1:8765';
-var H=[],MAX_HISTORY=50,HIST_IDX=-1;
-var cp='demo',cm='deepseek-v4-pro',cmd='normal',cperm='full',crea='high';
-var isDark=false,busy=false,curAI=null,cCost=0,cCache=0,cBal=0,cTok=0;
-var sStart=0,sChars=0,todos=[];
-var DEFAULT_MODELS=[['deepseek-v4-flash','V4 Flash'],['deepseek-v4-pro','V4 Pro']];
-var availModels=DEFAULT_MODELS.slice();
-function $(id){return document.getElementById(id);}
-function E(s){return(s||'').replace(/&/g,'&').replace(//g,'>');}
-
-// ── init ──
-document.addEventListener('DOMContentLoaded',function(){
- setupInput();setupKeyboard();setupDragDrop();
- loadModels();loadCfg();loadProjects();updateStatus();
-});
-
-// ── keyboard ──
-function setupKeyboard(){
- document.addEventListener('keydown',function(e){
- var t=e.target;
- if(t.tagName==='INPUT'||t.tagName==='TEXTAREA')return;
- if(e.key==='Escape'){
- closeQuickSettings();closeDetailSettings();
- if($('wiz-overlay')&&$('wiz-overlay').classList.contains('show'))return;
- }
- if(e.ctrlKey||e.metaKey){
- if(e.key==='b'||e.key==='B'){e.preventDefault();toggleSidebar();}
- if(e.key==='l'||e.key==='L'){e.preventDefault();var ib=$('input-box');if(ib)ib.focus();}
- if(e.key==='n'||e.key==='N'){e.preventDefault();newChat();}
- }
- });
-}
-
-// ── drag & drop ──
-function setupDragDrop(){
- var box=$('input-box');if(!box)return;
- ['dragenter','dragover','dragleave','drop'].forEach(function(ev){
- box.addEventListener(ev,function(e){e.preventDefault();e.stopPropagation();});
- });
- box.addEventListener('drop',function(e){
- var files=e.dataTransfer.files;
- if(files.length){var names=[];for(var i=0;i=0?Math.round(cCache)+'%':'--'],['wc-cost',cCost>=0?'¥'+cCost.toFixed(4):'--'],['wc-balance',cBal>0?'¥'+cBal.toFixed(2):'--']];
- s.forEach(function(p){var e=$(p[0]);if(e)e.textContent=p[1];});
-}
-function updateStatus(){
- var s=[['st-model',cm.includes('flash')?'DeepSeek V4 Flash':'DeepSeek V4 Pro'],['st-reason',crea],['st-mode',({normal:'Std',plan:'plan',ultra:'ultra'})[cmd]||cmd],['st-cost',cCost>=0?'¥'+cCost.toFixed(4):'--'],['st-tokens','tokens '+(cTok||0)],['st-cache',cCache>=0?'Cache '+Math.round(cCache)+'%':'--']];
- s.forEach(function(p){var e=$(p[0]);if(e)e.textContent=p[1];});
-}
-
-// ── quick settings ──
-function openQuickSettings(){buildQS();$('qs-overlay').classList.add('show');}
-function closeQuickSettings(){$('qs-overlay').classList.remove('show');}
-function buildQS(){
- var s=[
- {id:'qs-model',k:'model',c:cm,o:availModels},
- {id:'qs-reasoning',k:'reasoning_effort',c:crea,o:[['high','high'],['max','max']]},
- {id:'qs-permission',k:'permission_mode',c:cperm,o:[['reviewer','Reviewer'],['coder','Coder'],['full','Full']]},
- {id:'qs-mode',k:'chat_mode',c:cmd,o:[['normal','Standard'],['plan','plan'],['ultra','ultra']]}
- ];
- s.forEach(function(s){
- var c=$(s.id);if(!c)return;c.innerHTML='';
- s.o.forEach(function(o){
- var b=document.createElement('button');b.className='qs-btn'+(o[0]===s.c?' on':'');
- b.textContent=o[1];
- b.onclick=function(){var obj={};obj[s.k]=o[0];apiPut(obj);if(s.k==='model')cm=o[0];else if(s.k==='chat_mode')cmd=o[0];else if(s.k==='permission_mode')cperm=o[0];else crea=o[0];updateStatus();wcStats();buildQS();};
- c.appendChild(b);
- });
- });
-}
-
-// ── detail settings ──
-function openSettings(){switchDSTab('basic');$('ds-overlay').classList.add('show');
- apiGet('/api/config').then(function(c){if(!c)return;buildDSRows(c);fillDSCfg(c);});}
-function closeDetailSettings(){$('ds-overlay').classList.remove('show');}
-function fillDSCfg(c){
- if(!c)return;
- var nn=$('ds-nickname');if(nn){if(c.nickname)nn.value=c.nickname;nn.setAttribute('data-saved',nn.value||'');}
- var ak=$('ds-apikey');if(ak){ak.value=c.api_key_exists?'********':'';ak.setAttribute('data-saved',ak.value);}
- var bu=$('ds-baseurl');if(bu){if(c.base_url)bu.value=c.base_url;bu.setAttribute('data-saved',bu.value||'');}
-}
-function saveDSField(key, value){
- var el=document.getElementById('ds-'+({nickname:'nickname',api_key:'apikey',base_url:'baseurl'}[key]||key));
- // 如果是 api_key 且 value 为空且之前是脱敏状态,不保存
- if(key==='api_key'&&!value&&el&&el.getAttribute('data-saved')==='********')return;
- var saved=(el?el.getAttribute('data-saved'):null)||'';
- if(String(value)===saved)return;
- var o={};o[key]=value;apiPut(o);
- if(el)el.setAttribute('data-saved',String(value));
- // 保存反馈
- var t=el?el.nextElementSibling:null;
- if(t&&t.classList.contains('ds-saved')){t.textContent=I18N.t('saved_feedback');t.style.opacity='1';setTimeout(function(){t.style.opacity='0';},1500);}
-}
-function buildDSRows(c){
- if(!c)return;
- buildDSRow('ds-language',[['zh','Chinese'],['en','English']],'',c.language||'zh');
- buildDSRow('ds-theme',[['light','Light'],['dark','Dark'],['system','System']],'',c.theme||'light');
- buildDSRow('ds-model', availModels, 'model', cm);
- buildDSRow('ds-permission',[['reviewer','Reviewer'],['coder','Coder'],['full','Full']],'',c.permission_mode||'full');
-}
-function buildDSRow(id,opts,k2,curVal){
- var c=$(id);if(!c)return;c.innerHTML='';
- opts.forEach(function(o){
- var b=document.createElement('button');
- b.className='qs-btn'+(o[0]===curVal?' on':'');
- b.textContent=o[1];
- b.onclick=function(){
- var k=k2||(id==='ds-language'?'language':id==='ds-theme'?'theme':id==='ds-permission'?'permission_mode':id==='ds-model'?'model':'');
- var o2={};o2[k]=o[0];apiPut(o2);
- if(k==='language')I18N.setLang(o[0]);
- if(k==='model')cm=o[0];
- // 更新高亮
- var btns=c.querySelectorAll('.qs-btn');
- btns.forEach(function(b2){b2.classList.remove('on');});
- b.classList.add('on');
- };
- c.appendChild(b);
- });
-}
-function switchDSTab(t){
- document.querySelectorAll('.ds-tab').forEach(function(t2){t2.classList.toggle('active',t2.getAttribute('data-tab')===t);});
- ['basic','provider','about'].forEach(function(t2){var p=$('ds-tab-'+t2);if(p)p.style.display=(t2===t)?'block':'none';});
-}
-function addProvider(){alert(I18N.t('provider_coming_soon'));}
-
-// ── wizard ──
-var wizPg=0;
-var wizProvider='official';
-var wizCustomModels=[{name:'',context1m:true}];
-var wizModelCount=1;
-var WIZ_SEQ=['wiz-p0','wiz-p-provider','wiz-p-custom','wiz-p1','wiz-p2','wiz-p3'];
-function showWiz(i){
- for(var j=0;j0){
- o2.model=validModels[0].name.trim();
- o2.custom_models=validModels;
- }
- }
- fetch(API+'/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(o2)})
- .then(function(){$('wiz-overlay').classList.remove('show');loadCfg();loadProjects();})
- .catch(function(){$('wiz-overlay').classList.remove('show');loadCfg();loadProjects();});
- }
-}
-
-// ── wizard helpers ──
-function selectProvider(type){
- wizProvider=type;
- var off=$('wiz-prov-official'),cust=$('wiz-prov-custom');
- if(off)off.classList.toggle('selected',type==='official');
- if(cust)cust.classList.toggle('selected',type==='custom');
- var btn=$('wiz-btn-provider');if(btn)btn.disabled=false;
-}
-function updateCustomModelName(idx,name){
- if(idx=2)return;
- wizModelCount++;
- wizCustomModels.push({name:'',context1m:true});
- var idx=wizCustomModels.length-1;
- var container=$('wiz-model-rows');if(!container)return;
- var row=document.createElement('div');row.className='wiz-model-row';row.setAttribute('data-row',idx);
- var inp=document.createElement('input');inp.type='text';inp.className='wiz-model-name';
- inp.setAttribute('data-i18n-placeholder','wiz_model_name_ph');
- inp.placeholder=I18N.t('wiz_model_name_ph');
- inp.oninput=function(){updateCustomModelName(idx,this.value);};
- var tg=document.createElement('div');tg.className='wiz-toggle-group';tg.id='wiz-toggle-'+idx;
- var btn1M=document.createElement('button');btn1M.className='qs-btn on';btn1M.textContent='1M';
- btn1M.onclick=function(){updateCustomModelToggle(idx,true,btn1M);};
- var btnStd=document.createElement('button');btnStd.className='qs-btn';btnStd.textContent='Std';
- btnStd.onclick=function(){updateCustomModelToggle(idx,false,btnStd);};
- tg.appendChild(btn1M);tg.appendChild(btnStd);
- var del=document.createElement('button');del.className='wiz-model-del-btn';del.innerHTML='✕';
- del.onclick=function(){removeCustomModelRow(idx,row);};
- row.appendChild(inp);row.appendChild(tg);row.appendChild(del);
- container.appendChild(row);
- var addBtn=$('wiz-add-model');if(addBtn&&wizModelCount>=2)addBtn.style.display='none';
-}
-function removeCustomModelRow(idx,rowEl){
- wizCustomModels.splice(idx,1);wizModelCount--;
- if(rowEl&&rowEl.parentElement)rowEl.parentElement.removeChild(rowEl);
- var rows=document.querySelectorAll('#wiz-model-rows .wiz-model-row');
- for(var i=0;i_cmdsBaseLen){CMDS.pop();}
- availModels.forEach(function(m){
- CMDS.push({cmd:'/model '+m[1], icon:'🔀', name:'/model '+m[1],
- desc:'切换 '+m[1], zh:'切换 '+m[1], en:'Switch to '+m[1]});
- });
-}
-var cmdIdx=-1,cmdList=[];
-
-function showCmdPopup(){
- var box=$('input-box'),v=box?box.value:'';
- if(!v.startsWith('/')||v.length<1){hideCmdPopup();return;}
- var q=v.toLowerCase();cmdList=CMDS.filter(function(c){return c.cmd.toLowerCase().indexOf(q)===0;});
- if(cmdList.length===0){hideCmdPopup();return;}
- var p=$('cmd-popup');if(!p)return;
- p.innerHTML='';cmdIdx=-1;
- var isEn=I18N.current==='en';
- cmdList.forEach(function(c,i){
- var row=document.createElement('div');row.className='cmd-item';
- row.innerHTML=''+c.icon+' '+c.name+' '+(isEn?c.en:c.zh)+' ';
- row.onclick=function(){selectCmd(i);};
- row.onmousedown=function(e){e.preventDefault();};
- p.appendChild(row);
- });
- p.classList.add('show');
-}
-function hideCmdPopup(){var p=$('cmd-popup');if(p){p.classList.remove('show');p.innerHTML='';}cmdIdx=-1;cmdList=[];}
-function moveCmdSel(d){
- cmdIdx+=d;cmdIdx=Math.max(-1,Math.min(cmdList.length-1,cmdIdx));
- var rows=document.querySelectorAll('#cmd-popup .cmd-item');
- rows.forEach(function(r,i){r.classList.toggle('sel',i===cmdIdx);});
-}
-function selectCmd(idx){
- if(idx<0||idx>=cmdList.length)return;
- handleSlashCmd(cmdList[idx].cmd);
- hideCmdPopup();
-}
-function handleSlashCmd(cmd){
- if(cmd==='/dscodeisnice'){showEasterEgg();$('input-box').value='';updateSB();return;}
- if(cmd==='/clear'){clearMsgs();$('input-box').value='';updateSB();return;}
- if(cmd==='/help'){alert('DS Code v2.0.1\n/clear /plan /ultra /normal /dark /light /model /undo\n/dscodeisnice 🤫');return;}
- if(cmd==='/dark'){isDark=false;toggleTheme();$('input-box').value='';updateSB();return;}
- if(cmd==='/light'){isDark=true;toggleTheme();$('input-box').value='';updateSB();return;}
- // dynamic /model — iterate availModels
- if(cmd.lastIndexOf('/model ',0)===0){
- var target=cmd.slice(7).trim();
- for(var i=0;i=0)selectCmd(cmdIdx);else{hideCmdPopup();}return;}
- if(e.key==='Escape'){e.preventDefault();hideCmdPopup();return;}
- }
- if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();hideCmdPopup();sendMsg();}
- else if(e.key==='ArrowUp'&&!box.value){e.preventDefault();navHist(-1);}
- else if(e.key==='ArrowDown'&&!box.value){e.preventDefault();navHist(1);}
- });
- document.addEventListener('click',function(e){if(!e.target.closest('#input-area'))hideCmdPopup();});
-}
-function updateSB(){
- var box=$('input-box');if(!box)return;var has=(box.value||'').trim().length>0;
- var sb=$('btn-send');if(sb)sb.disabled=busy||!has;
- var st=$('btn-stop');if(st)st.style.display=busy?'flex':'none';
-}
-function navHist(dir){
- if(!H.length)return;
- if(HIST_IDX===-1)HIST_IDX=H.length;
- HIST_IDX=Math.max(0,Math.min(H.length,HIST_IDX+dir));
- var box=$('input-box');if(box)box.value=HIST_IDX0?Math.round(sChars/el):0;ts.textContent=cps+' c/s - '+sChars+' chars';
-}
-
-function showEasterEgg(){
- var o=$('ee-overlay');if(!o)return;o.classList.add('show');
- setTimeout(function(){o.classList.remove('show');},3000);
-}
-function sendMsg(){
- var box=$('input-box');if(!box)return;var text=(box.value||'').trim();if(!text)return;
- if(text==='/dscodeisnice'){box.value='';showEasterEgg();return;}
- if(busy){busy=false;updateSB();return;}
- // history
- if(H.length===0||H[H.length-1]!==text){H.push(text);if(H.length>MAX_HISTORY)H.shift();}
- HIST_IDX=-1;
- addBub('user',text);box.value='';box.style.height='auto';
- var wc=$('welcome-card');if(wc)wc.style.display='none';
- busy=true;updateSB();resetTS();
- var bub=addAI();curAI=bub;var done=false;
- var url=API+'/api/chat/stream?project='+encodeURIComponent(cp)+'&text='+encodeURIComponent(text);
- console.log('[sendMsg] EventSource url:',url);
- var es=new EventSource(url);
- var ct=setTimeout(function(){if(!done){var c=bub.querySelector('.ai-content');if(c&&!c.textContent.trim())c.textContent=I18N.t('waiting_response');}},10000);
- es.addEventListener('content',function(e){clearTimeout(ct);try{appendAI(bub,JSON.parse(e.data));}catch(er){appendAI(bub,e.data);}});
- es.addEventListener('reasoning',function(e){clearTimeout(ct);try{appendRsn(bub,JSON.parse(e.data));}catch(er){appendRsn(bub,e.data);}});
- es.addEventListener('tool_call',function(e){clearTimeout(ct);try{addTC(bub,JSON.parse(e.data));}catch(er){}});
- es.addEventListener('tool_result',function(e){clearTimeout(ct);try{endTC(bub,JSON.parse(e.data));}catch(er){}});
- es.addEventListener('new_turn',function(){/* typing indicator */});
- es.addEventListener('status',function(e){console.log('[status]',e.data);});
- es.addEventListener('srv_error',function(e){clearTimeout(ct);done=true;var c=bub.querySelector('.ai-content'),msg=I18N.t('error_server');try{msg=JSON.parse(e.data).message||msg;}catch(er){}if(c)c.textContent='❌ '+msg;busy=false;updateSB();});
- es.addEventListener('error',function(e){
- console.log('[EventSource] error, readyState='+es.readyState);
- es.close(); // 阻止 EventSource 自动重连
- if(es.readyState===EventSource.CLOSED||!done){
- clearTimeout(ct);done=true;
- var c=bub.querySelector('.ai-content');if(c&&!c.textContent.trim())c.textContent=I18N.t('connection_closed');
- busy=false;updateSB();
- }
- });
- es.addEventListener('done',function(e){
- clearTimeout(ct);es.close();done=true;
- try{var d=JSON.parse(e.data);cCost=d.cost||0;cCache=d.cache_hit_rate||0;cTok=d.total_tokens||0;updateStatus();wcStats();}catch(er){}
- var inner=bub.querySelector('.ai-inner');if(inner)inner.classList.add('done');
- updateTS();
- // copy + retry buttons
- var ac=document.createElement('div');ac.className='ai-actions';
- var cp2=document.createElement('button');cp2.textContent=I18N.t('btn_copy');cp2.onclick=function(){var t=bub.querySelector('.ai-content');if(t)navigator.clipboard.writeText(t.textContent).catch(function(){});};
- var re=document.createElement('button');re.textContent=I18N.t('btn_retry');re.onclick=function(){var p=bub.previousElementSibling;while(p&&!p.classList.contains('bubble-user'))p=p.previousElementSibling;if(p){$('input-box').value=p.textContent;sendMsg();}};
- ac.appendChild(cp2);ac.appendChild(re);bub.querySelector('.ai-inner').appendChild(ac);
- // auto-collapse reasoning
- var rsn=bub.querySelector('.rsn-block');if(rsn){var body=rsn.querySelector('.rsn-body');if(body&&!body.classList.contains('hidden')){body.classList.add('hidden');var hdr=rsn.querySelector('.rsn-hdr');if(hdr)hdr.innerHTML=I18N.t('thinking_done');}}
- busy=false;updateSB();scrollDown();
- // 刷新项目列表(自动创建的项目也能看到)
- loadProjects();
- });
-}
-function stopChat(){fetch(API+'/api/chat/stop',{method:'POST'}).catch(function(){});busy=false;updateSB();}
-
-// ── bubbles ──
-function addBub(role,text){var ct=$('chat-msgs');if(!ct)return;var el=document.createElement('div');el.className='bubble bubble-'+role;el.textContent=text;ct.appendChild(el);scrollDown();return el;}
-function addAI(){var ct=$('chat-msgs');if(!ct)return null;var el=document.createElement('div');el.className='bubble bubble-ai';el.innerHTML='
';ct.appendChild(el);scrollDown();return el;}
-function appendAI(bub,delta){sChars+=delta.length;var c=bub.querySelector('.ai-content');if(c){c.textContent+=delta;try{var md=renderMarkdown(c.textContent);if(md!==c.innerHTML)c.innerHTML=md;}catch(er){}}updateTS();scrollDown();}
-function appendRsn(bub,delta){
- var rsn=bub.querySelector('.rsn-block');if(!rsn){rsn=document.createElement('div');rsn.className='rsn-block';bub.insertBefore(rsn,bub.firstChild);}
- rsn.style.display='block';
- if(!rsn.querySelector('.rsn-hdr')){var hdr=document.createElement('div');hdr.className='rsn-hdr';hdr.innerHTML=I18N.t('thinking_label');hdr.onclick=function(){var b=this.parentElement.querySelector('.rsn-body');if(b){b.classList.toggle('hidden');this.innerHTML=b.classList.contains('hidden')?I18N.t('thinking_label'):I18N.t('thinking_expanded');}};rsn.appendChild(hdr);}
- var body=rsn.querySelector('.rsn-body');if(!body){body=document.createElement('div');body.className='rsn-body';rsn.appendChild(body);}
- body.textContent+=delta;scrollDown();
-}
-function addTC(bub,data){var name=data.tool||(data.function&&data.function.name)||'tool';var area=bub.querySelector('.tools-area');if(!area)return;var card=document.createElement('div');card.className='tool-card';card.setAttribute('data-name',name);card.innerHTML=' '+E(name)+'
';card.querySelector('.tool-hdr').onclick=function(){var b=card.querySelector('.tool-body');b.style.display=b.style.display==='block'?'none':'block';};area.appendChild(card);scrollDown();}
-function endTC(bub,data){var name=data.name||'tool';var area=bub.querySelector('.tools-area');if(!area)return;var card=area.querySelector('[data-name="'+name+'"]');if(!card)return;var dot=card.querySelector('.tool-dot');if(dot)dot.classList.add('ok');var body=card.querySelector('.tool-body');if(body){body.textContent=(data.result||'').slice(0,300);body.style.display='block';}}
-function clearMsgs(){var msgs=$('chat-msgs');if(!msgs)return;Array.prototype.slice.call(msgs.children).forEach(function(c){if(c.id!=='welcome-card')c.remove();});var wc=$('welcome-card');if(wc)wc.style.display='';wcStats();curAI=null;}
-function scrollDown(){
- var ct=$('chat-container');if(!ct)return;
- var bar=ct;var atBottom=(bar.scrollHeight-bar.scrollTop-bar.clientHeight)<40;
- if(atBottom)requestAnimationFrame(function(){ct.scrollTop=ct.scrollHeight;});
-}
-
-// ── todos ──
-function addTodoFromBtn(){var inp=$('todo-input');if(!inp||!inp.value.trim())return;todos.push({text:inp.value.trim(),done:false});inp.value='';renderTodos();}
-function toggleTodo(idx){todos[idx].done=!todos[idx].done;renderTodos();}
-function delTodo(idx){todos.splice(idx,1);renderTodos();}
-function renderTodos(){
- var list=$('todos-list');if(!list)return;list.innerHTML='';
- todos.forEach(function(t,i){var row=document.createElement('div');row.className='todo-row';row.innerHTML=''+E(t.text)+' x ';list.appendChild(row);});
- var cnt=$('todo-count');if(cnt)cnt.textContent=todos.length;
-}
-
-// ── files ──
-function openProjectFolder(){alert(I18N.t('project_files_path')+' '+cp+'/');}
-
-// ── utils ──
-function openExternal(url){var w=window.open(url,'_blank');if(!w)location.href=url;}
-
-// ── exports ──
-window.toggleTheme=toggleTheme;window.toggleSidebar=toggleSidebar;window.newChat=newChat;
-window.openSettings=openSettings;window.openQuickSettings=openQuickSettings;
-window.closeQuickSettings=closeQuickSettings;window.closeDetailSettings=closeDetailSettings;
-window.switchDSTab=switchDSTab;window.addProvider=addProvider;
-window.fillDSCfg=fillDSCfg;window.saveDSField=saveDSField;
-window.wizNext=wizNext;window.selectProvider=selectProvider;
-window.updateCustomModelName=updateCustomModelName;window.updateCustomModelToggle=updateCustomModelToggle;
-window.addCustomModelRow=addCustomModelRow;window.removeCustomModelRow=removeCustomModelRow;
-window.sendMessage=sendMsg;window.stopChat=stopChat;
-window.showEasterEgg=showEasterEgg;
-window.addTodoFromBtn=addTodoFromBtn;
-window.toggleTodo=toggleTodo;window.delTodo=delTodo;
-window.updateSB=updateSB;
-window.openProjectFolder=openProjectFolder;window.openExternal=openExternal;
-})();
diff --git a/DS code 2.0.1-0712-NEXT demo/renderer/js/i18n.js b/DS code 2.0.1-0712-NEXT demo/renderer/js/i18n.js
deleted file mode 100644
index f9b497f..0000000
--- a/DS code 2.0.1-0712-NEXT demo/renderer/js/i18n.js
+++ /dev/null
@@ -1,279 +0,0 @@
-// DScode V2 — 国际化 (zh/en)
-var I18N = {
- current: 'zh',
- strings: {
- zh: {
- sidebar_title: 'DS Code',
- new_chat: '+ 新对话',
- dark_mode: '🌙 深色模式',
- light_mode: '☀ 浅色模式',
- settings: '⚙ 详细设置',
- welcome_title: 'DS Code',
- welcome_subtitle: 'v2.0.1 NEXT — 一句话,让 AI 为你写代码',
- model: '模型',
- cache_hit: '缓存命中',
- cost: '本次成本',
- balance: '账户余额',
- input_placeholder: '输入消息… Enter 发送 Shift+Enter 换行',
- disclaimer: 'DS Code 是 AI 工具,可能出错,请核实重要信息',
- status_normal: '标准',
- status_plan: 'plan',
- status_ultra: 'ultra',
- status_cost_label: '本次成本',
- status_tokens_label: 'tokens',
- status_cache_label: '缓存',
- qs_title: '快捷设置',
- qs_model: '模型',
- qs_reasoning: '思考深度',
- qs_permission: '权限',
- qs_mode: '模式',
- qs_close: '关闭',
- qs_high: 'high',
- qs_max: 'max',
- qs_reviewer: '审查者',
- qs_coder: '码农',
- qs_full: '完全',
- qs_normal: '标准',
- qs_plan: 'plan',
- qs_ultra: 'ultra',
- ds_title: '详细设置',
- ds_tab_basic: '基本',
- ds_tab_provider: '供应商',
- ds_tab_about: '关于',
- ds_nickname: '用户昵称',
- ds_apikey: 'API Key',
- ds_baseurl: 'Base URL',
- ds_language: '语言',
- ds_theme: '主题模式',
- ds_permission: '权限模式',
- ds_session_path: '会话存档路径',
- ds_add_provider: '添加供应商',
- ds_prov_name_ph: '供应商名称',
- ds_prov_key_ph: 'API Key',
- ds_prov_url_ph: 'Base URL',
- ds_btn_add: '添加',
- ds_about_ver: 'DS Code v2.0.1-0712 NEXT',
- ds_about_desc: '基于 DeepSeek V4 API · Electron + Python Sidecar',
- ds_about_log: '日志路径: dscode_data/projects/',
- ds_about_disclaimer: 'DS Code 是一款 AI 编程助手工具,生成内容可能有误,请注意辨别。不对使用本工具产生的任何后果承担责任,请勿违反当地法律。请合理使用 AI,对输出内容保持审慎判断。',
- ds_about_terms: '用户协议',
- wiz_provider_title: '选择服务提供商',
- wiz_provider_desc: '您想如何连接 AI 服务',
- wiz_prov_official_name: 'DeepSeek 官方',
- wiz_prov_official_desc: '使用 DeepSeek 官方 API 服务',
- wiz_prov_custom_name: '自定义供应商',
- wiz_prov_custom_desc: '连接到您自己的 OpenAI 兼容端口',
- wiz_provider_btn: '继续 →',
- wiz_custom_title: '自定义供应商配置',
- wiz_baseurl_ph: 'https://api.example.com/v1',
- wiz_baseurl_hint: '*目前只支持OpenAI端口',
- wiz_model_info_title: '模型信息',
- wiz_model_name_ph: '模型名称',
- wiz_custom_btn: '继续 →',
- wiz_add_model_btn: '+ 添加模型',
- wiz_welcome_title: '欢迎使用 DS Code',
- wiz_welcome_desc: 'AI 编程助手,随时为你服务',
- wiz_welcome_btn: '开始设置 →',
- wiz_apikey_title: '输入你的 API Key',
- wiz_apikey_ph: 'sk-…',
- wiz_apikey_btn: '继续 →',
- wiz_nick_title: '我们可以怎么称呼您',
- wiz_nick_ph: '填写昵称…',
- wiz_nick_btn: '继续 →',
- wiz_disc_title: '使用前请确认',
- wiz_disc_1: 'DS Code 是一款 AI 工具,可能生成不准确内容,请注意辨别',
- wiz_disc_2: '不对使用本工具产生的任何后果承担责任,请勿违反当地法律',
- wiz_disc_3: '请合理使用 AI,对输出内容保持审慎判断',
- wiz_disc_link: '查看用户协议 →',
- wiz_disc_check: '我已阅读并同意以上内容',
- wiz_disc_btn: '开始使用 →',
- todo_title: '待办事项',
- todo_empty: '暂无待办',
- files_title: '文件列表',
- files_empty: '选择项目后显示文件',
- error_sidecar_down: '❌ 无法连接 Sidecar — 请在终端执行: python3 sidecar/sidecar.py',
- waiting_response: '正在等待 DeepSeek 回复…',
- error_server: '服务端错误',
- error_empty: '(空回复)',
- token_speed: '{cps} 字/秒 · {chars} 字符',
- cost_display: '¥{cost}',
- cache_display: '缓存 {rate}%',
- status_card_model: 'DeepSeek V4 {variant}',
- settings_tab_basic: '基本',
- settings_tab_provider: '供应商',
- settings_tab_about: '关于',
- settings_save_and_close: '关闭',
- expand_sidebar: '展开侧边栏',
- light_theme: '浅色',
- dark_theme: '深色',
- system_theme: '跟随系统',
- context_rename: '重命名',
- context_delete: '删除',
- context_pin: '置顶',
- new_project_prompt: '新项目名称:',
- btn_copy: '复制',
- btn_retry: '重试',
- project_files_path: '项目文件在: data/projects/',
- saved_feedback: '✓ 已保存',
- provider_coming_soon: '供应商管理 - 即将推出',
- connection_closed: '❌ 连接已关闭 — 请检查 Sidecar',
- thinking_label: '思考中',
- thinking_done: '思考完成',
- thinking_expanded: '思考中 ▾',
- todo_placeholder: '输入待办… 回车添加',
- },
- en: {
- sidebar_title: 'DS Code',
- new_chat: '+ New Chat',
- dark_mode: '🌙 Dark',
- light_mode: '☀ Light',
- settings: '⚙ Settings',
- welcome_title: 'DS Code',
- welcome_subtitle: 'v2.0.1 NEXT — AI that writes code for you',
- model: 'Model',
- cache_hit: 'Cache Hit',
- cost: 'Cost',
- balance: 'Balance',
- input_placeholder: 'Type a message… Enter to send, Shift+Enter for new line',
- disclaimer: 'DS Code is an AI tool and may make mistakes. Please verify important information.',
- status_normal: 'Standard',
- status_plan: 'plan',
- status_ultra: 'ultra',
- status_cost_label: 'Cost',
- status_tokens_label: 'tokens',
- status_cache_label: 'Cache',
- qs_title: 'Quick Settings',
- qs_model: 'Model',
- qs_reasoning: 'Reasoning',
- qs_permission: 'Permission',
- qs_mode: 'Mode',
- qs_close: 'Close',
- qs_high: 'high',
- qs_max: 'max',
- qs_reviewer: 'Reviewer',
- qs_coder: 'Coder',
- qs_full: 'Full',
- qs_normal: 'Standard',
- qs_plan: 'plan',
- qs_ultra: 'ultra',
- ds_title: 'Settings',
- ds_tab_basic: 'Basic',
- ds_tab_provider: 'Provider',
- ds_tab_about: 'About',
- ds_nickname: 'Nickname',
- ds_apikey: 'API Key',
- ds_baseurl: 'Base URL',
- ds_language: 'Language',
- ds_theme: 'Theme',
- ds_permission: 'Permission',
- ds_session_path: 'Session Path',
- ds_add_provider: 'Add Provider',
- ds_prov_name_ph: 'Provider Name',
- ds_prov_key_ph: 'API Key',
- ds_prov_url_ph: 'Base URL',
- ds_btn_add: 'Add',
- ds_about_ver: 'DS Code v2.0.1-0712 NEXT',
- ds_about_desc: 'Powered by DeepSeek V4 API · Electron + Python Sidecar',
- ds_about_log: 'Log path: dscode_data/projects/',
- ds_about_disclaimer: 'DS Code is an AI programming assistant. Generated content may contain errors — please verify. We are not responsible for any consequences arising from the use of this tool. Do not violate local laws. Use AI responsibly and maintain critical judgment of all output.',
- ds_about_terms: 'Terms of Service',
- wiz_provider_title: 'Choose Your Provider',
- wiz_provider_desc: 'Select how you want to connect',
- wiz_prov_official_name: 'DeepSeek Official',
- wiz_prov_official_desc: 'Use the official DeepSeek API service',
- wiz_prov_custom_name: 'Custom Provider',
- wiz_prov_custom_desc: 'Connect to your own OpenAI-compatible endpoint',
- wiz_provider_btn: 'Continue →',
- wiz_custom_title: 'Custom Provider Setup',
- wiz_baseurl_ph: 'https://api.example.com/v1',
- wiz_baseurl_hint: '*Currently only supports OpenAI-compatible endpoints',
- wiz_model_info_title: 'Model Info',
- wiz_model_name_ph: 'model-name',
- wiz_custom_btn: 'Continue →',
- wiz_add_model_btn: '+ model',
- wiz_welcome_title: 'Welcome to DS Code',
- wiz_welcome_desc: 'AI coding assistant, always at your service',
- wiz_welcome_btn: 'Get Started →',
- wiz_apikey_title: 'Enter your API Key',
- wiz_apikey_ph: 'sk-…',
- wiz_apikey_btn: 'Continue →',
- wiz_nick_title: 'What should we call you',
- wiz_nick_ph: 'Your nickname…',
- wiz_nick_btn: 'Continue →',
- wiz_disc_title: 'Before you start',
- wiz_disc_1: 'DS Code is an AI tool and may generate inaccurate content. Please verify.',
- wiz_disc_2: 'We are not responsible for any consequences of using this tool. Do not violate local laws.',
- wiz_disc_3: 'Use AI responsibly. Maintain critical judgment of all output.',
- wiz_disc_link: 'View Terms of Service →',
- wiz_disc_check: 'I have read and agree to the above',
- wiz_disc_btn: 'Start Using →',
- todo_title: 'To-Do',
- todo_empty: 'No tasks',
- files_title: 'Files',
- files_empty: 'Select a project to view files',
- error_sidecar_down: '❌ Cannot connect to Sidecar — run: python3 sidecar/sidecar.py',
- waiting_response: 'Waiting for DeepSeek response…',
- error_server: 'Server error',
- error_empty: '(empty response)',
- token_speed: '{cps} chars/s · {chars} chars',
- cost_display: '¥{cost}',
- cache_display: 'Cache {rate}%',
- status_card_model: 'DeepSeek V4 {variant}',
- settings_tab_basic: 'Basic',
- settings_tab_provider: 'Providers',
- settings_tab_about: 'About',
- settings_save_and_close: 'Close',
- expand_sidebar: 'Expand sidebar',
- light_theme: 'Light',
- dark_theme: 'Dark',
- system_theme: 'System',
- context_rename: 'Rename',
- context_delete: 'Delete',
- context_pin: 'Pin',
- new_project_prompt: 'New project:',
- btn_copy: 'Copy',
- btn_retry: 'Retry',
- project_files_path: 'Project files in: data/projects/',
- saved_feedback: '✓ Saved',
- provider_coming_soon: 'Provider mgmt - coming soon',
- connection_closed: '❌ Connection closed — check sidecar',
- thinking_label: 'Thinking',
- thinking_done: 'Thinking done',
- thinking_expanded: 'Thinking ▾',
- todo_placeholder: 'New todo… Enter to add',
- }
- },
- t: function(key) {
- var s = this.strings[this.current] || this.strings.zh;
- return s[key] || key;
- },
- setLang: function(lang) {
- if (this.strings[lang]) this.current = lang;
- else this.current = 'zh';
- this.refreshUI();
- return this.current;
- },
- refreshUI: function() {
- // Refresh all data-i18n elements
- var els = document.querySelectorAll('[data-i18n]');
- for (var i = 0; i < els.length; i++) {
- var el = els[i];
- var key = el.getAttribute('data-i18n');
- if (key) el.textContent = this.t(key);
- }
- // Placeholders
- var phs = document.querySelectorAll('[data-i18n-placeholder]');
- for (var i = 0; i < phs.length; i++) {
- var el = phs[i];
- var key = el.getAttribute('data-i18n-placeholder');
- if (key) el.placeholder = this.t(key);
- }
- // Title attributes
- var tts = document.querySelectorAll('[data-i18n-title]');
- for (var i = 0; i < tts.length; i++) {
- var el = tts[i];
- var key = el.getAttribute('data-i18n-title');
- if (key) el.title = this.t(key);
- }
- }
-};
diff --git a/DS code 2.0.1-0712-NEXT demo/renderer/js/utils.js b/DS code 2.0.1-0712-NEXT demo/renderer/js/utils.js
deleted file mode 100644
index 0124614..0000000
--- a/DS code 2.0.1-0712-NEXT demo/renderer/js/utils.js
+++ /dev/null
@@ -1,60 +0,0 @@
-// DScode V2 — 工具函数
-
-/** 安全的 HTML 转义 */
-function escapeHtml(str) {
- if (!str) return '';
- return String(str)
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/\n/g, ' ');
-}
-
-/** 简单的 Markdown 渲染(代码块 + 行内代码 + 粗体) */
-function renderMarkdown(text) {
- if (!text) return '';
- let result = escapeHtml(text);
-
- // 代码块 ```...```
- result = result.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
- return `${escapeHtml(code.trim())} `;
- });
-
- // 行内代码 `...`
- result = result.replace(/`([^`]+)`/g, '$1');
-
- // 粗体 **...**
- result = result.replace(/\*\*([^*]+)\*\*/g, '$1 ');
-
- return result;
-}
-
-/** 截断字符串 */
-function truncate(str, maxLen = 200) {
- if (!str) return '';
- return str.length > maxLen ? str.slice(0, maxLen) + '...' : str;
-}
-
-/** 格式化工具名(双语) */
-function formatToolName(name) {
- var map = {
- 'read_file': '读取文件|Read File',
- 'list_files': '列出文件|List Files',
- 'create_file': '创建文件|Create File',
- 'write_file': '写入文件|Write File',
- 'modify_file': '修改文件|Modify File',
- 'edit_file': '编辑文件|Edit File',
- 'mark_delete': '标记删除|Mark Delete',
- 'delete_file': '删除文件|Delete File',
- 'run_command': '执行命令|Run Command',
- 'web_search': '网页搜索|Web Search',
- 'web_fetch': '抓取网页|Web Fetch',
- 'git_status': 'Git 状态|Git Status',
- 'git_diff': 'Git 差异|Git Diff',
- 'git_commit': 'Git 提交|Git Commit',
- };
- var entry = map[name] || name;
- var parts = entry.split('|');
- return (I18N.current === 'en') ? (parts[1] || name) : (parts[0] || name);
-}
diff --git a/DS code 2.0.1-0712-NEXT demo/start.sh b/DS code 2.0.1-0712-NEXT demo/start.sh
old mode 100644
new mode 100755
index f9784ff..7446b36
--- a/DS code 2.0.1-0712-NEXT demo/start.sh
+++ b/DS code 2.0.1-0712-NEXT demo/start.sh
@@ -1,47 +1,4 @@
-#!/bin/bash
-# DScode V2 — 一键启动脚本
-# 启动 Sidecar + Electron
-
-SIDECAR_PORT=8765
-DIR="$(cd "$(dirname "$0")" && pwd)"
-
-echo "🚀 正在启动 DScode V2..."
-echo ""
-
-# 1. 杀掉占用端口的旧进程
-echo "[1/3] 清理旧进程..."
-lsof -ti:$SIDECAR_PORT 2>/dev/null | xargs kill -9 2>/dev/null
-sleep 0.5
-
-# 2. 启动 Sidecar(后台)
-echo "[2/3] 启动 Python Sidecar..."
-cd "$DIR"
-python3 sidecar/sidecar.py &
-SIDECAR_PID=$!
-
-# 3. 等待就绪
-echo "[3/3] 等待 Sidecar 就绪..."
-for i in $(seq 1 30); do
- sleep 0.3
- if curl -s http://127.0.0.1:$SIDECAR_PORT/api/health > /dev/null 2>&1; then
- echo "✅ Sidecar 已就绪(PID=$SIDECAR_PID)"
- echo ""
- echo "🎯 启动 Electron..."
- # 按下 Ctrl+C 时同时杀掉 sidecar
- trap "echo ''; echo '正在关闭...'; kill $SIDECAR_PID 2>/dev/null; exit" SIGINT SIGTERM
- npx electron . --dev
- break
- fi
-done
-
-# 如果循环结束还没就绪
-if ! curl -s http://127.0.0.1:$SIDECAR_PORT/api/health > /dev/null 2>&1; then
- echo "❌ Sidecar 启动超时,请检查端口 $SIDECAR_PORT"
- kill $SIDECAR_PID 2>/dev/null
- exit 1
-fi
-
-# 关闭 Electron 后清理
-echo "关闭中..."
-kill $SIDECAR_PID 2>/dev/null
-echo "👋 已退出"
+#!/usr/bin/env bash
+set -euo pipefail
+DSCODE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+exec "$DSCODE_ROOT/start.sh" "$@"
diff --git a/DS code 2.0.1-0712-NEXT demo/stop.sh b/DS code 2.0.1-0712-NEXT demo/stop.sh
old mode 100644
new mode 100755
index fa4a309..59cd6b3
--- a/DS code 2.0.1-0712-NEXT demo/stop.sh
+++ b/DS code 2.0.1-0712-NEXT demo/stop.sh
@@ -1,7 +1,4 @@
-#!/bin/bash
-# DScode V2 — 一键停止
-SIDECAR_PORT=8765
-echo "🛑 正在停止 DScode..."
-lsof -ti:$SIDECAR_PORT 2>/dev/null | xargs kill -9 2>/dev/null && echo "✅ Sidecar 已停止" || echo "ℹ️ Sidecar 未运行"
-pkill -f "npx electron" 2>/dev/null && echo "✅ Electron 已关闭" || echo "ℹ️ Electron 未运行"
-echo "👋 已退出"
+#!/usr/bin/env bash
+set -euo pipefail
+DSCODE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+exec "$DSCODE_ROOT/stop.sh" "$@"
diff --git a/README.md b/README.md
index 128e21e..de1ac1c 100644
--- a/README.md
+++ b/README.md
@@ -1,98 +1,88 @@
# DS Code v2.0.1-0712-NEXT
-> AI 编程助手 · Electron + Python Sidecar · DeepSeek V4 API
+AI 编程助手:Electron 界面、Python Sidecar、ChatEngine 与 OpenAI 兼容 API。
-## 快速启动 / Quick Start
+## 安装与启动
+
+需要 Node.js 22+、npm、Python 3.10+。从仓库根目录运行:
```bash
-cd "DS code 2.0.1-0712-NEXT demo"
+python3 -m venv .venv
+.venv/bin/python -m pip install -r requirements.lock
+npm ci
./start.sh
```
-首次启动会自动下载 Electron,之后秒开。
+macOS/Linux 使用 `./start.sh`;Windows 安装依赖后运行 `npm start`,虚拟环境 Python 位于 `.venv/Scripts/python.exe`。`requirements.txt` 定义依赖范围,`requirements.lock` 保存本次验证的精确版本。
+
+MCP、图片处理与旧 Qt 界面需要可选运行依赖:`.venv/bin/python -m pip install -r requirements-optional.lock`。开发和完整验证使用 `requirements-dev.txt`,其中包含可选依赖、Ruff 和 coverage。Ubuntu 上 Qt 还需要 `libegl1`、`libopengl0` 系统库。
+
+Electron 负责启动一个 Sidecar、检查 HTTP 健康状态、随后加载界面;关闭窗口时只清理自己创建的进程。端口已占用会报告启动失败。`npm run dev` 打开开发工具,不会另起一个 Sidecar。旧 demo 目录中的启动脚本会转发到根目录。
+
+可通过 `DSCODE_PYTHON` 选择 Python、`DSCODE_SIDECAR_PORT` 选择端口、`DSCODE_DATA_DIR` 指定用户数据目录。没有覆盖时使用 platformdirs 的系统用户数据目录。配置写入该目录的 `config.json`,敏感字段使用 Fernet 加密;数据不保存在仓库。
-## 目录结构 / File Structure
+## 结构
+```text
+ electron/ 桌面窗口、受限 preload、Sidecar 进程管理
+ renderer/ HTML/CSS/JS、聊天客户端、异步确认对话框
+ sidecar/http_protocol.py HTTP 请求边界、JSON 与 SSE 编码
+ sidecar/runtime.py 单引擎请求管理、事件隔离、取消与确认
+ sidecar/service.py 配置、项目、历史与聊天 API
+ sidecar/project_data.py 持久任务、文件预览与附件上传
+ sidecar/providers.py 供应商添加、切换、删除与脱敏
+ sidecar/sidecar.py 本地服务器入口
+ engine/src/ 引擎、工具、沙箱、记忆、旧 Qt 界面
+ tests/ 本地回归与模拟供应商集成测试
+ docs/DELIVERY.md 源码依据、故障证据、修复及验证范围
```
-├── start.sh # 一键启动脚本
-├── stop.sh # 停止脚本
-├── sidecar/ # Python Sidecar (纯 stdlib HTTP/SSE)
-│ ├── sidecar.py # 主程序 (端口 8765)
-│ ├── sidecar_engine.py # 引擎封装 (备用)
-│ └── sidecar_api.py # FastAPI 版本 (备用)
-├── renderer/ # 前端界面
-│ ├── index.html # 主页面
-│ ├── js/
-│ │ ├── app.js # 主逻辑 (向导/聊天/设置)
-│ │ ├── i18n.js # 中英文国际化
-│ │ └── utils.js # Markdown 渲染 / 工具函数
-│ └── css/
-│ ├── theme.css # 设计令牌 + 深色模式
-│ └── app.css # 布局 + 组件样式
-├── engine/src/ # AI 引擎
-│ ├── core/ # ChatEngine / DeepSeek API / Config / Hooks
-│ ├── tools/ # 91 内置工具
-│ ├── sandbox/ # 项目隔离 / 历史记录
-│ ├── prompts/ # 系统提示词
-│ └── agents/ # 子代理
-├── electron/ # Electron 窗口
-├── data/ # 配置和数据
-├── assets/ # 图标/图片
-└── node_modules/ # 依赖 (npm install 后)
+
+## API 开发
+
+独立调试可运行 `python3 -m sidecar.sidecar --port 8765`。只监听 `127.0.0.1`。桌面模式自动生成本次进程的认证令牌;独立调试可通过 `DSCODE_SIDECAR_TOKEN` 配置令牌。设置令牌后 JSON API 需要 `Authorization: Bearer `,EventSource 流需要认证头或 `token` 参数。API 不接受任意网站 Origin。
+
+| 端点 | 行为 |
+| --- | --- |
+| `GET /api/health` | 服务身份与健康状态 |
+| `GET/PUT /api/config` | 读取脱敏配置 / 校验后原子保存 |
+| `GET/POST /api/projects` | 项目列表 / 创建并返回规范名称 |
+| `GET /api/projects/{name}/history` | 最新持久化对话 |
+| `PUT /api/projects/{name}` | 保存 `pinned` 或 `display_name` |
+| `GET/PUT /api/projects/{name}/tasks` | 读取 / 保存任务;PUT 包含读取时的 `revision`,冲突返回 409 |
+| `GET /api/projects/{name}/files` | work 与 preview 文件列表 |
+| `GET /api/projects/{name}/file?path=...` | 安全文本或图片预览 |
+| `POST /api/projects/{name}/uploads` | `{name,data}`,data 为文件真实内容的 base64 |
+| `DELETE /api/projects/{name}` | 删除项目;处理中返回 409 |
+| `GET/POST /api/providers` | 脱敏供应商列表 / 添加供应商 |
+| `PUT/DELETE /api/providers/{index}` | `{active:true}` 切换 / 删除供应商;处理中返回 409 |
+| `GET /api/models` | 自定义模型配置或供应商模型列表 |
+| `GET /api/balance` | 供应商余额;区分货币 |
+| `POST /api/chat/send` | `{project,text,attachments?:[上传返回的名称]}` 创建请求,返回 `run_id` |
+| `GET /api/chat/stream?run_id=...` | 接收该请求的 SSE,不重复启动模型 |
+| `POST /api/chat/stop` | `{run_id}` 取消指定请求 |
+| `POST /api/chat/confirm` | `{run_id,request_id,kind,value}` 回复命令/编辑/问答确认 |
+| `POST /api/chat/plan` | `{project,confirmed}` 接受或拒绝待执行计划 |
+
+保留旧的 `GET /api/chat/stream?project=...&text=...` 兼容入口。一个引擎同时处理一个请求;重复请求返回 409,一个流只允许一个接收端。断线、超时会取消该请求;事件缓冲有上限。终止事件是 `done` 或 `srv_error`。
+
+Todo 和 Pin 持久化到项目数据;Todo 使用 revision 防止旧页面覆盖新任务。文件对话框支持文本和 PNG/JPEG/GIF/WebP 预览,上传每个文件不超过 512 KiB。每次对话最多 8 个附件、合计 2 MiB;UTF-8 文本内容与图片会进入模型请求。PDF、DOCX 和其他二进制附件会明确报错。
+
+设置中的供应商高级选项可以指定 `thinking`、`reasoning_effort`、原生工具、usage 和 vision 能力;通用供应商默认不会收到 DeepSeek 专用参数。`context_tokens` 设置请求预算(1024–1048576),按保守估算删除较早的完整对话组,保留工具调用与结果的配对,不修改落盘历史。它不是供应商实际窗口大小的保证;`context_1m` 仅为模型元数据。自动重试只发生在输出前,已收到部分回复后出错会保留该回复并报告错误。
+
+## 验证
+
+```bash
+.venv/bin/python -m pip install -r requirements-dev.txt
+PYTHONPATH=engine .venv/bin/python -m unittest discover -s tests -v
+npm test
+.venv/bin/python -m ruff check engine sidecar tests
+QT_QPA_PLATFORM=offscreen .venv/bin/python tests/qt_smoke.py
```
-## 功能 / Features
-
-### 对话 / Chat
-- DeepSeek V4 Pro / Flash 流式对话
-- Thinking 模式:实时推理链输出
-- 对话历史自动保存与恢复
-
-### 引导页 / Onboarding Wizard
-- 选择服务提供商:DeepSeek 官方 / 自定义供应商
-- 自定义供应商支持:Base URL + 模型配置 (最多 2 个)
-- 1M Context 开关 per model
-
-### 工具调用 / Tool Calls
-- 91 个内置工具 (文件操作 / 命令执行 / Git / 搜索 / MCP / …)
-- 原生 DeepSeek V4 function calling (`tools` 参数)
-- 文本提取 fallback 兼容旧格式
-
-### UI
-- `/` 斜杠命令菜单
-- 中英文双语 (实时切换)
-- 深色/浅色主题
-- 项目侧边栏 (创建/切换/删除)
-- 待办事项、文件管理面板
-
-### API 端点 / API Endpoints
-| 端点 | 说明 |
-|---|---|
-| `GET /api/health` | 健康检查 |
-| `GET /api/config` | 读取配置 |
-| `PUT /api/config` | 更新配置 |
-| `GET /api/projects` | 列出项目 |
-| `POST /api/projects` | 创建项目 |
-| `DELETE /api/projects/{name}` | 删除项目 |
-| `GET /api/chat/stream` | SSE 流式对话 |
-| `POST /api/chat/stop` | 停止对话 |
-| `GET /api/balance` | 查询余额 (http.client) |
-| `GET /api/models` | 列出模型 |
-
-## 配置 / Configuration
-
-首次启动会弹出引导页。配置加密存储在 `~/dscode_data/config.enc` (Fernet)。
-
-## 技术栈 / Tech Stack
-
-| 层 | 技术 |
-|---|---|
-| 前端 | HTML/CSS/JS (vanilla, zero framework) |
-| 后端 | Python 3.9+ asyncio stdlib (zero pip install) |
-| 引擎 | OpenAI SDK + http.client |
-| 流式 | Server-Sent Events (SSE) |
-| 配置 | Fernet 加密存储 |
-
-## License
-
-Apache-2.0
+有桌面显示环境时还可运行 `.venv/bin/python tests/electron_smoke.py`;Linux 无显示时使用 `xvfb-run -a .venv/bin/python tests/electron_smoke.py`。它使用本地模拟供应商打开真实 Electron 窗口,验证 IPC、认证、项目创建、Todo/Pin 持久化、原生工具、文件预览、真实附件、上下文设置、流式代码块、刷新后历史与工具卡片恢复,以及退出后的 Sidecar 清理;截图保存在 `test-results/electron.png`。
+
+GitHub Actions 执行完整依赖安装、Python/JavaScript 回归、Qt/Electron 冒烟、Ruff、语法检查及覆盖率采集。集成测试运行真实 Sidecar、ChatEngine 和 OpenAI SDK,并连接本地模拟供应商,不调用收费 API。
+
+模块审计、复现证据和验证限制见 [交付报告](docs/DELIVERY.md)。Qt 界面不是当前 Electron 启动入口。
+
+Apache-2.0,见 LICENSE。
diff --git a/data/config.json b/data/config.json
deleted file mode 100644
index ded737c..0000000
--- a/data/config.json
+++ /dev/null
@@ -1 +0,0 @@
-{"api_key":"","base_url":"https://api.deepseek.com","model":"deepseek-v4-pro","available_models":[{"name":"deepseek-v4-pro","label":"DeepSeek-V4-Pro","description":"更强推理","primary":true},{"name":"deepseek-v4-flash","label":"DeepSeek-V4-Flash","description":"快速响应","primary":false}],"language":"zh","theme":"light","reasoning_effort":"high","chat_mode":"normal","nickname":"","onboarding_completed":false,"permission_mode":"full","last_project":null,"balance_cny":0,"cache_hit_rate":0,"total_tokens_used":0}
diff --git a/docs/DELIVERY.md b/docs/DELIVERY.md
new file mode 100644
index 0000000..ed8a4e8
--- /dev/null
+++ b/docs/DELIVERY.md
@@ -0,0 +1,72 @@
+# DS Code 调试与重构交付
+
+日期:2026-10-11。仓库:。审阅入口:。
+
+## 源码依据与记忆限制
+
+GitHub 默认分支 `main` 的最新提交为 `6d07c34629fa5a68a451055f68009e8d784e8be4`,提交时间 2026-07-12 09:59:32 UTC。已通过 GitHub 连接器及 `git ls-remote` 独立核对,交付前再次确认 main 未变化。另一分支 `DS-code-2.0.1-0711-NEXT` 已合入 main;本次直接使用 main 源码,修复提交位于 `fix/runtime-refactor-20261011`,精确交付 SHA 与文件校验值见交付包旁的 `manifest.json`。
+
+当前系统可用工具、资源与工作区未提供历史分析记忆的读取入口或 DS Code 记忆文件,无法恢复此前会话的问题清单。仓库没有独立 issues 清单,只有已合并的 PR #1。本次结论来自最新源码、故障复现和回归证据;源码中的审计编号不作为已读取历史报告的证据。
+
+## 已修复问题与结构调整
+
+| 范围 | 复现或不足 | 交付行为 |
+| --- | --- | --- |
+| 启动入口 | README 指向不完整 demo;存在重复前端和 Sidecar 实现 | 根目录统一启动,兼容脚本转发,拆出 HTTP 协议、请求运行时、API、项目数据与供应商服务。 |
+| 桌面生命周期 | Electron 未调用后端启动;脚本按端口杀进程 | Electron 持有并认证自己创建的 child;健康检查验证服务身份,退出只清理该 child,POSIX 父进程消失时后端退出。 |
+| HTTP | 请求头/body 分包时无效 JSON;读取被截断,错误状态不准确 | 按 Content-Length 完整读取,限制头/body/时限,处理真实状态码、Origin 和认证。 |
+| 聊天运行时 | 全局缓冲跨请求串流;重复发送、旧确认污染下一轮 | 独立 run、SSE 单接收端、并发 409、确认 ID、取消 Event、事件上限与断线清理。 |
+| 确认与计划 | 前端确认没有后端闭环,接受计划不能继续执行 | 命令/编辑/问答回复绑定具体 run;接受计划后继续,停止唤醒待确认调用。 |
+| 配置与凭据 | 分字段保存留下半批配置;active_provider 保留旧字段;Qt 快捷设置与实际请求分歧 | 批量及旧属性入口统一同步活动供应商,失败恢复内存;空密钥不回退旧凭据,能力/预算不跨供应商继承,Fernet 加密与 API 脱敏。 |
+| 供应商管理 | Provider 页面是占位内容 | 添加、切换、删除供应商接入真实 API;切换同步模型、能力和预算,保留初始配置、限制数量,运行中拒绝变更。 |
+| SDK 连接 | 新旧 client 共用诊断 session,关闭旧连接影响新 client | SDK 各自持有 transport,诊断独立;模型/余额使用完整 scheme、port、base path 与 TLS 配置。 |
+| 重试与输出 | SDK、API 与引擎叠加重试;部分流失败可能重复输出/执行 | 统一有界重试,SDK 自动重试关闭;只有输出前可重试,部分输出后错误保留文本并报告。 |
+| 模型能力 | 所有兼容 API 都收到 DeepSeek 专用参数 | 显式 thinking/reasoning/tools/usage/vision 配置,通用模型默认不加 DeepSeek 专用字段;文本与图片共用同一流消费和工具处理。 |
+| 原生工具 | usage 尾块覆盖合并的 tool_calls;工具 schema 不完整 | 保留参数分片与 usage;补齐 18 个描述,共 109 个原生工具契约;历史只输出 API 标准字段和匹配的 tool_call_id。 |
+| 文本工具解析 | 旧正则在 200 个合法 JSON 变体中失败 174 个 | 替换为有长度/数量上限的线性 JSON 边界解析,正确处理字段顺序、嵌套、转义和代码内容;拒绝残缺或非法对象。 |
+| 工具执行 | 大型 if 链、钩子收到空参数、reviewer Git 操作可绕过写限制 | 路由表明确注册;钩子收到实际参数;reviewer 与子代理写限制覆盖 Git 变更操作;公开 shutdown 释放持久 shell。 |
+| 终端/命令 | 小输出阻塞读取;非零退出被报告成功;进程组终止可能影响宿主 | 及时读取可用输出,真实退出码决定成功,超时/取消关闭持有终端;只终止自己创建的进程组。 |
+| 项目元数据 | Pin 写入却返回 None;保存失败被伪装成功 | Pin、显示名、任务与上传使用共享原子写入;失败保留旧文件并返回真实结果。 |
+| 项目文件边界 | 项目/internal symlink 可导致外部创建 DS.md;rename 覆盖空目录;写限制按字符 | 拒绝相关符号链接与路径逃逸,拒绝覆盖已有项目;统一项目级锁,按 UTF-8 字节限制写入。 |
+| Todo | 完成 `fix api` 同时误删 `fix api timeout`;前端任务只存内存 | 精确匹配任务;任务整批持久化,revision 冲突返回 409,桌面刷新后恢复。 |
+| 文件与附件 | 文件管理仅提示路径,拖放仅附文件名;恢复历史时附件上下文直接显示在用户气泡 | 文件列表、安全文本/图片预览、真实字节上传;文本和图片进入 SDK 请求,界面保留原始输入,完整模型上下文仍落盘。 |
+| 历史 | /clear 不清引擎;项目切换不显示历史;同秒新会话覆盖 | 清空并持久化、恢复历史;会话名增加纳秒标识,输入/返回/快照深拷贝;写入失败保留旧文件和待保存状态。 |
+| 上下文 | 无预算;取消的工具调用可能缺结果 | 保守 UTF-8 token 估算、图片配额、输出预留;删除较早完整对话组,补齐未完成工具结果,不改磁盘历史;当前消息过大时明确报错。 |
+| 余额与 UI | 官方 balance_infos 未解析;零余额/其他币种显示错误;Markdown 流重复转义 | 正确读取余额、区分币种;保留原始流文本,围栏一次转义;工具失败卡片不再显示成功。 |
+| 引擎状态 | mixin 通配导入隐藏依赖;余额刷新全局状态;旧项目索引后台覆盖新项目 | 9 个引擎模块改显式导入,余额锁与状态归属实例;项目 generation 阻止过期结果回写,切换/退出关闭工具资源。 |
+| MCP | ClientSession 未进入 context,握手挂起;不同任务退出 SDK context | 同一持久任务持有并退出 stdio/session;完成 handshake、list、call、restart 和清理。 |
+| LSP | URI 编码/空格错误;缓冲读取等待大块数据 | Path URI 正确编解码,及时读取帧,限制 body;真实本地协议进程完成 initialize/exit。 |
+| 钩子与记忆 | blocking hook 非零退出被忽略;嵌套记忆对象可被外部修改 | 钩子失败实际阻止操作,并移除 Sidecar token 环境变量;记忆返回深拷贝,保存失败不污染内存。 |
+| 旧 Qt | 15 处缺失导入;推理气泡缺少动画字段导致消息创建失败 | 修复名称与初始化;158 个 Python 模块实际导入,创建窗口、用户/助手/工具/计划气泡与任务/文件控件。 |
+| 可复现构建 | 无完整锁文件、缺 CI,仓库跟踪零散 node_modules | npm/Core Python/可选 Python 锁文件,统一 Ruff 规则,自动回归、Qt/Electron 冒烟与覆盖率产物;清除重复和依赖副本。 |
+
+## 验证证据
+
+本地最终验证为 **66 项 Python 测试、11 项 JavaScript 测试通过**。Ruff(E9/F821/F822/F823/F601/F811/B006/B012/B023)、compileall、所有桌面与测试 JS 语法、Bash 语法及 `git diff --check` 通过;源码额外完成 Python 3.10 AST 语法检查。
+
+| 层级 | 实际执行范围 |
+| --- | --- |
+| HTTP/运行时 | 请求分包、大 UTF-8 body、认证/Origin、错误长度、并发拒绝、过期确认、取消、终止事件、POSIX 父进程退出。 |
+| 模型集成 | 真实 Sidecar → ChatEngine → OpenAI SDK → 本地模拟供应商:SSE、原生 create_file、工具结果回传、usage、文本/图片附件、模型和余额自定义路径。 |
+| 项目/记忆 | Pin、精确任务、revision、文件/快照回滚、symlink、rename、UTF-8 字节限制、保存失败、加密与脱敏、嵌套数据隔离、同秒多会话。 |
+| 进程协议 | 真实 FastMCP 测试服务握手、list/call、重启/关闭;本地 LSP framed transport;持久 shell 小输出、退出码、超时与资源释放。 |
+| 分析/工具 | 109 个工具的注册、方法与 JSON 参数描述契约;文件工具/快照、reviewer 权限、钩子阻止;真实代码解析/校验与关键词索引;200 个固定种子的文本工具变体。 |
+| JavaScript | 流结束/错误、重复发送、旧事件、POST 期间停止、过期对话框、Markdown、项目操作与附件字节、Electron child 启动/失败/退出。 |
+| Qt | offscreen 下导入 158 个引擎模块并实际创建窗口、消息/计划/任务/文件控件;无真实外部模型调用。 |
+| Electron | Linux/Xvfb + Playwright 打开真实窗口;IPC/认证、新项目、Todo/Pin 持久化、原生工具、文件预览、真实附件、上下文设置、Markdown、刷新历史、零页面脚本错误、退出后后端端口关闭。 |
+
+覆盖率采集包含 Python 集成测试的子进程,排除 Qt GUI:约 **32% 的整个 Sidecar/engine Python 可执行行**得到测试。新上下文预算模块为 100%,原子写入 97%,HTTP 协议与运行时约 93%,文本工具解析约 92%。这是行覆盖率,不是功能无缺陷证明;109 个工具完成注册契约检查,并未逐个完成所有业务场景。精确计数见交付的 coverage.xml。
+
+验证环境:Linux x86_64、Python 3.12.14、Node.js 24.19.0、Electron 33.4.11、OpenAI SDK 2.54.0、httpx 0.28.1、Playwright 1.64.0、MCP 1.26.0、PySide6 6.10.2。Electron 保留原 33 主版本;可选依赖与开发工具精确版本见锁文件。MCP SDK 的 Pydantic lifespan forward-reference 警告仍会出现,未阻止本地握手与调用;未通过屏蔽警告伪装解决。
+
+GitHub Actions 对最终提交安装完整依赖并执行同类检查,结果和截图/覆盖率产物见 PR #2 的 Checks。交付不修改 main,也未部署真实供应商或生产环境。
+
+## 明确保留的限制
+
+1. 无真实供应商凭据,未验证收费服务、实际价格与特定模型能力;预算采用保守估算而非供应商 tokenizer。`context_1m` 不保证 1M 支持,供应商 capability 由配置指定。
+2. 本次桌面附件支持 UTF-8 文本和 PNG/JPEG/GIF/WebP;每个 512 KiB、每轮最多 8 个/合计 2 MiB。PDF/DOCX 等二进制通过此入口明确拒绝,尚未接入文档提取流水线。
+3. MCP/LSP 的本地协议集成与代表工具已验证,未跑遍所有第三方服务器、语言服务、子代理协作和工具业务场景。神经嵌入/重型向量依赖未安装,本次仅验证关键词后备索引。
+4. ChatEngine 仍使用 mixin,剩余大型模块可在新增独立测试后继续按资源职责拆分。内部记忆与配置仍保留部分旧兼容实现,本次未将整个仓库重写为新架构。
+5. Windows/macOS 原生执行、安装包/签名/自动更新、Electron 新主版本升级尚未验证;多个独立安装同时运行仍需指定不同端口。
+
+如之后提供历史记忆入口,可将此前结论逐条映射到基线、复现测试与交付提交;本报告不将不可访问的历史问题宣称为已关闭。
diff --git a/electron/main.js b/electron/main.js
index e313754..1609db7 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -1,133 +1,72 @@
-const { app, BrowserWindow, ipcMain } = require('electron');
-const { spawn } = require('child_process');
-const path = require('path');
-const net = require('net');
-
-const SIDECAR_PORT = 8765;
-let sidecarProcess = null;
+const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
+const path = require('node:path');
+const fs = require('node:fs');
+const { randomBytes } = require('node:crypto');
+const { SidecarProcess } = require('./sidecar-process');
+
+const root = path.join(__dirname, '..');
+const localPython = path.join(root, '.venv', process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python');
+const port = Number(process.env.DSCODE_SIDECAR_PORT || 8765);
+const token = randomBytes(32).toString('hex');
+const sidecar = new SidecarProcess({
+ python: process.env.DSCODE_PYTHON || (fs.existsSync(localPython) ? localPython : 'python3'),
+ script: path.join(root, 'sidecar', 'sidecar.py'), cwd: root, port, token,
+});
let mainWindow = null;
+let canQuit = false;
-// ── Python Sidecar 生命周期管理 ──────────────────────
-function startSidecar() {
- // 先杀掉所有占用 SIDECAR_PORT 的旧进程
+function openExternal(url) {
try {
- const { execSync } = require('child_process');
- execSync(`lsof -ti:${SIDECAR_PORT} | xargs kill -9 2>/dev/null || true`, { timeout: 3000 });
+ const parsed = new URL(url);
+ if (['http:', 'https:'].includes(parsed.protocol)) shell.openExternal(url);
} catch {}
-
- console.log('[Electron] Starting Python sidecar...');
- const sidecarPath = path.join(__dirname, '..', 'sidecar', 'sidecar.py');
- const cwd = path.join(__dirname, '..');
-
- sidecarProcess = spawn('python3', [sidecarPath, '--port', String(SIDECAR_PORT)], {
- cwd: cwd,
- stdio: ['pipe', 'pipe', 'pipe'],
- });
-
- sidecarProcess.stdout.on('data', (data) => {
- console.log(`[sidecar] ${data.toString().trim()}`);
- });
-
- sidecarProcess.stderr.on('data', (data) => {
- console.error(`[sidecar:err] ${data.toString().trim()}`);
- });
-
- sidecarProcess.on('close', (code) => {
- console.log(`[Electron] Sidecar exited (code=${code})`);
- sidecarProcess = null;
- });
-}
-
-function stopSidecar() {
- if (sidecarProcess) {
- console.log('[Electron] Stopping sidecar...');
- sidecarProcess.kill('SIGTERM');
- setTimeout(() => {
- if (sidecarProcess) {
- sidecarProcess.kill('SIGKILL');
- }
- }, 3000);
- }
-}
-
-function waitForSidecar(retries = 30) {
- return new Promise((resolve, reject) => {
- const check = () => {
- const socket = new net.Socket();
- socket.setTimeout(500);
- socket.connect(SIDECAR_PORT, '127.0.0.1', () => {
- socket.destroy();
- resolve();
- });
- socket.on('error', () => {
- socket.destroy();
- if (retries-- > 0) {
- setTimeout(check, 500);
- } else {
- reject(new Error('Sidecar 启动超时'));
- }
- });
- };
- check();
- });
}
-// ── 主窗口 ─────────────────────────────────────────
-async function createWindow() {
+function createWindow() {
mainWindow = new BrowserWindow({
- width: 1280,
- height: 860,
- minWidth: 900,
- minHeight: 600,
- title: 'DS Code',
- backgroundColor: '#F8F9FB',
+ width: 1280, height: 860, minWidth: 900, minHeight: 600,
+ title: 'DS Code', backgroundColor: '#F8F9FB',
webPreferences: {
- nodeIntegration: false,
- contextIsolation: true,
+ nodeIntegration: false, contextIsolation: true, sandbox: true,
+ preload: path.join(__dirname, 'preload.js'),
},
});
-
- // 等待 sidecar 就绪
- try {
- await waitForSidecar();
- console.log('[Electron] Sidecar ready, loading UI...');
- } catch (e) {
- console.error('[Electron] Sidecar did not start:', e.message);
- }
-
- // 加载前端
- mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'index.html'));
-
- // DevTools(开发模式)
- if (process.argv.includes('--dev')) {
- mainWindow.webContents.openDevTools();
- }
-
- mainWindow.on('closed', () => {
- mainWindow = null;
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
+ openExternal(url);
+ return { action: 'deny' };
+ });
+ mainWindow.webContents.on('will-navigate', (event, url) => {
+ event.preventDefault(); openExternal(url);
});
+ mainWindow.loadFile(path.join(root, 'renderer', 'index.html'));
+ if (process.argv.includes('--dev')) mainWindow.webContents.openDevTools();
+ mainWindow.on('closed', () => { mainWindow = null; });
}
-// ── IPC handlers ───────────────────────────────────
-ipcMain.handle('get-sidecar-url', () => {
- return `http://127.0.0.1:${SIDECAR_PORT}`;
-});
-
-// ── App 生命周期 ───────────────────────────────────
-app.whenReady().then(async () => {
- await createWindow();
+ipcMain.handle('get-sidecar-config', event => {
+ if (!mainWindow || event.sender !== mainWindow.webContents) throw new Error('Unknown renderer');
+ return { url: sidecar.url, token };
});
-app.on('before-quit', () => {
- stopSidecar();
-});
-
-app.on('window-all-closed', () => {
+if (!app.requestSingleInstanceLock()) {
app.quit();
-});
-
-app.on('activate', () => {
- if (BrowserWindow.getAllWindows().length === 0) {
- createWindow();
- }
-});
+} else {
+ app.on('second-instance', () => { mainWindow?.show(); mainWindow?.focus(); });
+ app.whenReady().then(async () => {
+ try {
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Invalid DSCODE_SIDECAR_PORT');
+ await sidecar.start();
+ createWindow();
+ } catch (error) {
+ dialog.showErrorBox('DS Code 启动失败', error.message);
+ app.quit();
+ }
+ });
+ app.on('activate', () => { if (!mainWindow && sidecar.child) createWindow(); });
+ app.on('window-all-closed', () => app.quit());
+ app.on('before-quit', event => {
+ if (canQuit) return;
+ event.preventDefault();
+ sidecar.stop().finally(() => { canQuit = true; app.quit(); });
+ });
+}
diff --git a/electron/preload.js b/electron/preload.js
new file mode 100644
index 0000000..11e4d17
--- /dev/null
+++ b/electron/preload.js
@@ -0,0 +1,4 @@
+const { contextBridge, ipcRenderer } = require('electron');
+contextBridge.exposeInMainWorld('dscodeApi', {
+ bootstrap: () => ipcRenderer.invoke('get-sidecar-config'),
+});
diff --git a/electron/sidecar-process.js b/electron/sidecar-process.js
new file mode 100644
index 0000000..000c74c
--- /dev/null
+++ b/electron/sidecar-process.js
@@ -0,0 +1,75 @@
+const { spawn } = require('node:child_process');
+const http = require('node:http');
+
+function health(url, token) {
+ return new Promise((resolve, reject) => {
+ const request = http.get(`${url}/api/health`, { headers: { Authorization: `Bearer ${token}` } }, response => {
+ let body = '';
+ response.on('data', chunk => { body += chunk; });
+ response.on('end', () => {
+ try {
+ const data = JSON.parse(body);
+ if (response.statusCode !== 200 || data.service !== 'dscode-sidecar') throw new Error('Sidecar health check failed');
+ resolve(data);
+ } catch (error) { reject(error); }
+ });
+ response.on('error', reject);
+ });
+ request.setTimeout(500, () => request.destroy(new Error('Sidecar health check timed out')));
+ request.on('error', reject);
+ });
+}
+
+class SidecarProcess {
+ constructor({ python, script, cwd, port, token, spawnProcess = spawn, checkHealth = health }) {
+ Object.assign(this, { python, script, cwd, port, token, spawnProcess, checkHealth });
+ this.url = `http://127.0.0.1:${port}`;
+ this.child = null;
+ this.stopping = null;
+ }
+
+ async start(timeout = 15000) {
+ if (this.child) throw new Error('Sidecar is already running');
+ const child = this.spawnProcess(this.python, [this.script, '--port', String(this.port)], {
+ cwd: this.cwd, env: { ...process.env, DSCODE_SIDECAR_TOKEN: this.token,
+ DSCODE_PARENT_PID: String(process.pid), PYTHONUNBUFFERED: '1' },
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+ this.child = child;
+ let failure = null;
+ child.on('error', error => { failure = error; });
+ child.on('close', code => {
+ failure = failure || new Error(`Sidecar exited (${code}); check Python dependencies and port ${this.port}`);
+ if (this.child === child) this.child = null;
+ });
+ child.stdout?.on('data', data => console.log(`[sidecar] ${data.toString().trim()}`));
+ child.stderr?.on('data', data => console.error(`[sidecar] ${data.toString().trim()}`));
+ const deadline = Date.now() + timeout;
+ while (Date.now() < deadline) {
+ if (failure) throw failure;
+ try {
+ await this.checkHealth(this.url, this.token);
+ if (failure) throw failure;
+ return;
+ } catch (error) {
+ if (failure) throw failure;
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ }
+ throw new Error(`Sidecar startup timed out on port ${this.port}`);
+ }
+
+ stop() {
+ if (this.stopping) return this.stopping;
+ const child = this.child;
+ if (!child) return Promise.resolve();
+ this.stopping = new Promise(resolve => {
+ const timer = setTimeout(() => child.kill('SIGKILL'), 3000);
+ child.once('close', () => { clearTimeout(timer); resolve(); });
+ child.kill('SIGTERM');
+ }).finally(() => { this.stopping = null; });
+ return this.stopping;
+ }
+}
+
+module.exports = { SidecarProcess, health };
diff --git a/engine/src/core/atomic_files.py b/engine/src/core/atomic_files.py
new file mode 100644
index 0000000..fae988c
--- /dev/null
+++ b/engine/src/core/atomic_files.py
@@ -0,0 +1,49 @@
+"""Small atomic file writer shared by project data and local API uploads."""
+
+import os
+import tempfile
+from pathlib import Path
+
+
+def atomic_write_bytes(path: Path, payload: bytes) -> bool:
+ target = Path(path)
+ if target.is_symlink() or target.parent.is_symlink():
+ return False
+ directory = None
+ temp_name = None
+ try:
+ if os.open in os.supports_dir_fd and os.rename in os.supports_dir_fd:
+ directory = os.open(target.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
+ | getattr(os, "O_NOFOLLOW", 0))
+ temp_name = f".tmp_{os.getpid()}_{os.urandom(12).hex()}"
+ fd = os.open(temp_name, os.O_WRONLY | os.O_CREAT | os.O_EXCL
+ | getattr(os, "O_NOFOLLOW", 0), 0o600, dir_fd=directory)
+ with os.fdopen(fd, "wb") as stream:
+ stream.write(payload)
+ stream.flush()
+ os.fsync(stream.fileno())
+ os.replace(temp_name, target.name, src_dir_fd=directory, dst_dir_fd=directory)
+ else:
+ # Windows has no directory-fd API; still never fall back to a partial write.
+ fd, temp_name = tempfile.mkstemp(prefix=".tmp_", dir=target.parent)
+ with os.fdopen(fd, "wb") as stream:
+ stream.write(payload)
+ stream.flush()
+ os.fsync(stream.fileno())
+ if target.is_symlink() or target.parent.is_symlink():
+ return False
+ os.replace(temp_name, target)
+ return True
+ except (OSError, ValueError, TypeError):
+ return False
+ finally:
+ if temp_name:
+ try:
+ if directory is not None:
+ os.unlink(temp_name, dir_fd=directory)
+ else:
+ os.unlink(temp_name)
+ except OSError:
+ pass
+ if directory is not None:
+ os.close(directory)
diff --git a/engine/src/core/chat_engine/_api_mixin.py b/engine/src/core/chat_engine/_api_mixin.py
index b23df86..4ad3f42 100644
--- a/engine/src/core/chat_engine/_api_mixin.py
+++ b/engine/src/core/chat_engine/_api_mixin.py
@@ -2,7 +2,18 @@
"""APIStreamingMixin — 模型调用与流式响应处理(拆分自 chat_engine.py)。"""
from __future__ import annotations
-from src.core.chat_engine._imports import * # noqa: F401, F403
+from src.core.chat_engine._imports import (
+ APIError,
+ Dict,
+ List,
+ Optional,
+ get_global_rate_limiter,
+ time,
+)
+
+import logging
+
+logger = logging.getLogger(__name__)
@@ -43,33 +54,10 @@ def _call_model(self, api_messages: List[Dict]) -> Dict:
除 APIError 外,也会捕获连接/超时/网络等可重试异常,避免底层网络抖
动直接中断深度模式循环。重试次数与退避上限由类常量控制(C-001)。
"""
- last_error: Optional[APIError] = None
- for attempt in range(1, self._MAX_MODEL_RETRIES + 1):
- try:
- return self._stream_chat(api_messages)
- except APIError as e:
- last_error = e
- if 400 <= e.code < 500 and e.code != 429:
- raise
- if not self._should_retry_step(e):
- raise
- except Exception as e:
- # 非 APIError(如 ConnectionError / TimeoutError)按关键字判断是否可重试
- if not self._should_retry_step(e):
- raise
- last_error = APIError(0, str(e))
-
- if attempt >= self._MAX_MODEL_RETRIES:
- break
- delay = self._retry_delay(attempt)
- logger.warning(
- "_call_model 失败 (code=%s) 第 %s 次尝试,%ss 后重试",
- last_error.code if last_error else 0, attempt, delay,
- )
- time.sleep(delay)
- if last_error is not None:
- raise last_error
- raise APIError(500, "_call_model 重试耗尽")
+ if self._is_stopped():
+ raise APIError(499, "请求已被用户取消", need_retry=False)
+ # DeepSeekAPI owns transport retries. Never replay a partially delivered stream here.
+ return self._stream_chat(api_messages)
def _stream_chat(self, api_messages: List[Dict]) -> Dict:
"""调用 DeepSeek API,流式输出 content / reasoning,返回 usage。"""
diff --git a/engine/src/core/chat_engine/_base.py b/engine/src/core/chat_engine/_base.py
index b733197..a652900 100644
--- a/engine/src/core/chat_engine/_base.py
+++ b/engine/src/core/chat_engine/_base.py
@@ -7,7 +7,44 @@
"""
from __future__ import annotations
-from src.core.chat_engine._imports import * # noqa: F401, F403
+from src.core.chat_engine._imports import (
+ APIError,
+ Any,
+ ArtifactManager,
+ Callable,
+ Config,
+ ConfirmationGate,
+ ConversationHistory,
+ DEFAULT_MAX_TURN_TIME,
+ DeepSeekAPI,
+ Dict,
+ FileSystemMonitor,
+ KunRuntime,
+ List,
+ Optional,
+ Path,
+ ProjectSandbox,
+ ToolExecutor,
+ _ImageDocumentManagerLike,
+ _MCPManagerLike,
+ _SklIIIManagerLike,
+ _SubAgentEngineLike,
+ json,
+ re,
+ threading,
+ time,
+)
+
+import logging
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from src.core.auto_memory import AutoMemory
+ from src.core.long_term_memory import LongTermMemory
+ from src.core.session_memory import SessionMemory
+ from src.core.user_profile_memory import UserProfileMemory
+
+logger = logging.getLogger(__name__)
@@ -23,6 +60,11 @@ class ChatEngineBase(object):
_MAX_TOOL_RETRIES = 1
def __init__(self):
+ self._balance_lock = threading.RLock()
+ self._balance_last_call = 0.0
+ self._balance_running = False
+ self._project_generation = 0
+ self._closed = False
self.cfg = Config()
self.api = DeepSeekAPI()
# 核心依赖使用真实类型,便于 mypy 与 IDE 静态检查,避免 object 上的未知属性调用
@@ -44,6 +86,7 @@ def __init__(self):
# L-014: 防止发送按钮重复提交,确保同一时刻只有一个 send_message 在执行
self._send_lock = threading.Lock()
self._send_in_progress: bool = False
+ self._active_cancel_event = None
# 受 _state_lock 保护的标志统一放在 _state dict,避免分散的 torn read。
# 使用 Dict[str, Any] 因为内部存储多种类型(bool/str/float/list/None),
# 所有读取都通过 _state_cost / _state_int 等带类型转换的辅助方法完成。
@@ -74,7 +117,7 @@ def __init__(self):
self._subagent: Optional[_SubAgentEngineLike] = None # 子代理引擎(懒加载,set_project 中初始化)
# 用户可能显式发送 "/plan" "/undo" 等命令 — 由本类拦截处理
self._COMMANDS = {"/clear", "/reset", "/undo", "/snapshot", "/restore",
- "/snapshots", "/plan", "/ultra", "/help", "/hook", "/lsp"}
+ "/snapshots", "/plan", "/ultra", "/normal", "/help", "/hook", "/lsp"}
# ========== 回调接口 (GUI 或 CLI 自行设置) ==========
self.on_status_update: Optional[Callable[[str], None]] = None
@@ -193,7 +236,33 @@ def stop(self):
def _is_stopped(self) -> bool:
"""读取停止标志 — 线程安全(一次加锁读取避免 torn read)"""
with self._state_lock:
- return bool(self._state.get("stop_requested")) or bool(self._state.get("stop"))
+ return (bool(self._state.get("stop_requested")) or bool(self._state.get("stop"))
+ or (self._active_cancel_event is not None and self._active_cancel_event.is_set()))
+
+ def cancel_pending_confirmations(self):
+ self._gate.cancel_all()
+
+ def get_pending_plan(self):
+ with self._state_lock:
+ return self._state.get("pending_plan")
+
+ def shutdown(self):
+ """Release background services and transports when the host exits."""
+ self._closed = True
+ self._project_generation += 1
+ self.stop()
+ self._fs_monitor.stop()
+ resources = ((self.mcp_manager, "stop_all"), (self.lsp_manager, "shutdown_all"),
+ (self._subagent, "stop_all"), (self._subagent, "stop_watchdog"))
+ for manager, method in resources:
+ if manager is not None and hasattr(manager, method):
+ try:
+ getattr(manager, method)()
+ except Exception:
+ logger.exception("关闭后台服务失败: %s", method)
+ if self.tool_exec is not None:
+ self.tool_exec.shutdown()
+ self.api.close()
def _get_model(self) -> str:
"""获取当前生效的模型名称。
@@ -319,8 +388,10 @@ def _accumulate_usage(self, usage: Dict) -> None:
logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
# ========== 项目切换 ==========
- def set_project(self, sandbox: ProjectSandbox):
+ def set_project(self, sandbox: Optional[ProjectSandbox]):
"""设置当前项目"""
+ self._project_generation += 1
+ self._code_vector_store = None
# P1-9 修复: 切换项目前完整清理旧资源,防止MCP/子代理/文件监视器泄漏
# stop() 已负责归档旧会话(mark_stopped + save_checkpoint + archive + 停止checkpoint线程)
self.stop()
@@ -367,7 +438,16 @@ def set_project(self, sandbox: ProjectSandbox):
except Exception:
logger.debug("set_project 清理旧 subagent 异常", exc_info=True)
+ if self.tool_exec is not None:
+ self.tool_exec.shutdown()
self.sandbox = sandbox
+ if sandbox is None:
+ self._fs_monitor.stop()
+ self.history = self.tool_exec = None
+ self.mcp_manager = self.lsp_manager = self._subagent = None
+ self.memory_manager = self.auto_memory = self.long_term_memory = None
+ self.sklii_manager = self.image_doc_manager = self.hook_manager = self.skill_manager = None
+ return
self.history = ConversationHistory(sandbox.history_dir)
# MCP 管理器 (懒加载)
@@ -672,20 +752,23 @@ def _maybe_init_code_vector_store(self):
work_dir = getattr(self.sandbox, "work_dir", None)
if not work_dir:
return
+ sandbox, generation = self.sandbox, self._project_generation
# 代码向量索引初始化移至后台线程,避免 sentence-transformers/torch
# 在 NumPy 不兼容环境下导入时 segfault 导致主进程崩溃。
def _init_cv():
try:
from src.analysis.code_vector_store import CodeVectorStore
store = CodeVectorStore(work_dir)
- self._code_vector_store = store
- self._update_code_index_status(store.status)
store.index(force=False)
+ if self._closed or self.sandbox is not sandbox or self._project_generation != generation:
+ return
+ self._code_vector_store = store
self._update_code_index_status(store.status)
except Exception as _exc:
logger.info("代码向量索引初始化失败: %s", _exc)
- self._code_vector_store = None
- self._update_code_index_status("unavailable")
+ if not self._closed and self.sandbox is sandbox and self._project_generation == generation:
+ self._code_vector_store = None
+ self._update_code_index_status("unavailable")
self._update_code_index_status("initializing")
threading.Thread(target=_init_cv, daemon=True, name="CodeVectorInit").start()
@@ -782,7 +865,7 @@ def is_sending(self) -> bool:
return self._send_in_progress
def send_message(self, user_text: str, uploaded_files: Optional[List[str]] = None,
- images: Optional[List[str]] = None):
+ images: Optional[List[str]] = None, cancel_event=None, display_text=None):
"""发送用户消息入口。防止按钮重复提交,若已有请求在处理中则直接拒绝。"""
if not self._send_lock.acquire(blocking=False):
logger.warning("拒绝重复发送:已有请求正在处理中")
@@ -791,13 +874,17 @@ def send_message(self, user_text: str, uploaded_files: Optional[List[str]] = Non
return
self._send_in_progress = True
try:
- return self._send_message_impl(user_text, uploaded_files, images)
+ self._active_cancel_event = cancel_event
+ if cancel_event is not None and cancel_event.is_set():
+ return
+ return self._send_message_impl(user_text, uploaded_files, images, display_text)
finally:
+ self._active_cancel_event = None
self._send_in_progress = False
self._send_lock.release()
def _send_message_impl(self, user_text: str, uploaded_files: Optional[List[str]] = None,
- images: Optional[List[str]] = None):
+ images: Optional[List[str]] = None, display_text=None):
"""发送用户消息 -> 斜杠命令或两阶段 (Planning / Execution)
对 history/sandbox/API 调用异常均有防御,确保中途异常不会让 history 内存状态与磁盘不一致。
@@ -971,7 +1058,7 @@ def _watch_turn():
self.on_error(0, "历史记录未初始化")
return
try:
- self.history.add_user_message(full_user_text)
+ self.history.add_user_message(full_user_text, display_content=display_text if display_text is not None else user_text)
except Exception as e:
print(f"[ChatEngine] history.add_user_message 失败: {e}")
if self.on_error:
diff --git a/engine/src/core/chat_engine/_callback_mixin.py b/engine/src/core/chat_engine/_callback_mixin.py
index 6a1f3c1..d4c80e4 100644
--- a/engine/src/core/chat_engine/_callback_mixin.py
+++ b/engine/src/core/chat_engine/_callback_mixin.py
@@ -1,13 +1,20 @@
# -*- coding: utf-8 -*-
-"""CallbackMixin — 工具/事件回调、状态栏与余额刷新、健康检查(拆分自 chat_engine.py)。
-
-注意:``_refresh_status_bar`` 使用 ``global _balance_*``,这些模块级变量由
-``_protocols`` 定义并在此导入;``global`` 语句绑定到本模块命名空间,所有读写
-均在本模块内完成,状态一致。
-"""
+"""Callbacks and instance-scoped status refresh for ChatEngine."""
from __future__ import annotations
-from src.core.chat_engine._imports import * # noqa: F401, F403
+from src.core.chat_engine._imports import (
+ Any,
+ ConfirmationError,
+ Dict,
+ List,
+ Optional,
+ threading,
+ time,
+)
+
+import logging
+
+logger = logging.getLogger(__name__)
@@ -238,40 +245,39 @@ def _refresh_status_bar(self, usage: Dict):
f"模型: {self.cfg.model} | 缓存命中率: {hit_rate:.0f}% | 本次成本: \u00a5{cost:.4f} | 查询余额中..."
)
+ generation, callback = self._project_generation, self.on_status_update
def _async_check_balance():
- global _balance_check_running
try:
info = self.api.check_balance()
avail = info.get("available_balance", 0)
- if self.on_status_update:
- self.on_status_update(
- f"模型: {self.cfg.model} | 缓存命中率: {hit_rate:.0f}% | 本次成本: \u00a5{cost:.4f} | 余额: \u00a5{avail:.2f}"
+ currency = info.get("currency", "CNY")
+ if callback and not self._closed and generation == self._project_generation:
+ callback(
+ f"模型: {self.cfg.model} | 缓存命中率: {hit_rate:.0f}% | 本次成本: \u00a5{cost:.4f} | 余额: {currency} {avail:.2f}"
)
except Exception as _exc:
logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
finally:
- with _balance_running_lock:
- _balance_check_running = False
+ with self._balance_lock:
+ self._balance_running = False
# 防抖:避免高频调用创建过多线程(至少间隔 5 秒)
- # 使用模块级锁 + 运行中标志,避免多线程竞态条件
- global _balance_debounce_last_call, _balance_check_running
- with _balance_debounce_lock:
+ # 每个引擎分别记录刷新状态,避免跨实例互相阻塞。
+ with self._balance_lock:
now = time.monotonic()
- if now - _balance_debounce_last_call < 5.0:
+ if now - self._balance_last_call < 5.0:
return
# 检查是否已有余额查询线程在运行中
- with _balance_running_lock:
- if _balance_check_running:
- return
- _balance_check_running = True
- _balance_debounce_last_call = now
+ if self._balance_running:
+ return
+ self._balance_running = True
+ self._balance_last_call = now
try:
threading.Thread(target=_async_check_balance, daemon=True).start()
except Exception:
# 线程启动失败时重置标志,避免余额查询功能永久失效
- with _balance_running_lock:
- _balance_check_running = False
+ with self._balance_lock:
+ self._balance_running = False
# ========== 控制方法 ==========
def check_health(self) -> Dict:
diff --git a/engine/src/core/chat_engine/_command_mixin.py b/engine/src/core/chat_engine/_command_mixin.py
index 3fdd488..02c0f21 100644
--- a/engine/src/core/chat_engine/_command_mixin.py
+++ b/engine/src/core/chat_engine/_command_mixin.py
@@ -2,7 +2,15 @@
"""SlashCommandMixin — 斜杠命令解析与分发(拆分自 chat_engine.py)。"""
from __future__ import annotations
-from src.core.chat_engine._imports import * # noqa: F401, F403
+from src.core.chat_engine._imports import (
+ Optional,
+ Tuple,
+ time,
+)
+
+import logging
+
+logger = logging.getLogger(__name__)
@@ -93,6 +101,7 @@ def _handle_slash_command(self, cmd: str, args: list) -> bool:
"/snapshots": self._cmd_snapshots,
"/plan": self._cmd_plan,
"/ultra": self._cmd_ultra,
+ "/normal": self._cmd_normal,
"/help": self._cmd_help,
"/hook": self._cmd_hook,
"/lsp": self._cmd_lsp,
@@ -108,6 +117,8 @@ def _cmd_clear(self, args: list) -> bool:
"""处理 /clear 与 /reset:清空聊天(保留项目状态)。"""
if self.on_chat_clear_requested:
self.on_chat_clear_requested()
+ if self.history is not None:
+ self.history.clear()
if self.on_finished:
self.on_finished(0.0, 0.0, 0, 0)
return True
@@ -207,16 +218,23 @@ def _cmd_snapshots(self, args: list) -> bool:
def _cmd_plan(self, args: list) -> bool:
"""处理 /plan:强制进入 planning 模式并产生计划。"""
# 强制进入 planning 模式并产生计划
- with self._state_lock:
- self._state["phase"] = "planning"
- self._state["plan_confirmed"] = False
+ self.set_mode("plan")
+ self.cfg.set("chat_mode", "plan")
return False # 让 send_message 走正常流程(会在 planning 模式下产计划)
def _cmd_ultra(self, args: list) -> bool:
"""处理 /ultra:切换到 Ultra 模式(规划验证交付闭环)。"""
self.set_mode("ultra")
+ self.cfg.set("chat_mode", "ultra")
return False # 让消息走正常流程(会在 ultra 模式下走 _run_ultra_loop)
+ def _cmd_normal(self, args: list) -> bool:
+ self.set_mode("normal")
+ self.cfg.set("chat_mode", "normal")
+ if self.on_finished:
+ self.on_finished(0.0, 0.0, 0, 0)
+ return True
+
def _cmd_help(self, args: list) -> bool:
"""处理 /help:显示可用命令列表。"""
help_text = (
diff --git a/engine/src/core/chat_engine/_deep_loop_mixin.py b/engine/src/core/chat_engine/_deep_loop_mixin.py
index c2b5453..b8299f1 100644
--- a/engine/src/core/chat_engine/_deep_loop_mixin.py
+++ b/engine/src/core/chat_engine/_deep_loop_mixin.py
@@ -2,7 +2,19 @@
"""DeepLoopMixin — 深度循环(单轮/步骤/工具执行/计划阶段)核心循环(拆分自 chat_engine.py)。"""
from __future__ import annotations
-from src.core.chat_engine._imports import * # noqa: F401, F403
+from src.core.chat_engine._imports import (
+ APIError,
+ Dict,
+ List,
+ Optional,
+ TOOL_CALL_THRESHOLD,
+ Tuple,
+ json,
+)
+
+import logging
+
+logger = logging.getLogger(__name__)
@@ -228,7 +240,7 @@ def _execute_tool_calls_in_turn(self, tool_calls: List[Dict], usage: Dict) -> bo
except Exception as _exc:
logger.debug("on_tool_result 回调异常: %s", _exc, exc_info=True)
if self.history is not None:
- self.history.add_tool_result(name, user_answer, call.get("id", ""))
+ self.history.add_tool_result(name, user_answer, call.get("id", ""), ok=True)
# 会话短时记忆:用户回答也应记录,保证链路一致
try:
if self.session_memory is not None:
@@ -243,7 +255,7 @@ def _execute_tool_calls_in_turn(self, tool_calls: List[Dict], usage: Dict) -> bo
except Exception as _exc:
logger.debug("on_tool_result 回调异常: %s", _exc, exc_info=True)
if self.history is not None:
- self.history.add_tool_result(name, result, call.get("id", ""))
+ self.history.add_tool_result(name, result, call.get("id", ""), ok=ok)
# 会话短时记忆:记录工具调用摘要并立即 checkpoint
try:
if self.session_memory is not None:
@@ -487,7 +499,7 @@ def _execute_step_tools(self, tool_calls: List[Dict],
except Exception as _exc:
logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
if self.history is not None:
- self.history.add_tool_result(name, user_answer, call.get("id", ""))
+ self.history.add_tool_result(name, user_answer, call.get("id", ""), ok=True)
continue
if name == "exit_conversation":
@@ -505,7 +517,7 @@ def _execute_step_tools(self, tool_calls: List[Dict],
except Exception as _exc:
logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
if self.history is not None:
- self.history.add_tool_result(name, result, call.get("id", ""))
+ self.history.add_tool_result(name, result, call.get("id", ""), ok=ok)
# 会话短时记忆:记录深度模式工具结果并 checkpoint
try:
@@ -732,7 +744,7 @@ def reject_plan(self):
self._state["plan_confirmed"] = False
self._state["pending_plan"] = None
- def continue_from_plan(self):
+ def continue_from_plan(self, cancel_event=None):
"""计划确认后继续执行;如果未实现或未确认,回落到标准 send_message。"""
try:
with self._state_lock:
@@ -759,7 +771,7 @@ def continue_from_plan(self):
# 使用完 pending_plan 后清空,避免后续执行阶段被污染
with self._state_lock:
self._state["pending_plan"] = None
- self.send_message(prompt)
+ self.send_message(prompt, cancel_event=cancel_event)
except Exception as e:
try:
if self.on_error:
diff --git a/engine/src/core/chat_engine/_hook_mixin.py b/engine/src/core/chat_engine/_hook_mixin.py
index 962d87a..9c87834 100644
--- a/engine/src/core/chat_engine/_hook_mixin.py
+++ b/engine/src/core/chat_engine/_hook_mixin.py
@@ -2,7 +2,9 @@
"""HookCommandMixin — 自定义 Hook 命令处理(拆分自 chat_engine.py)。"""
from __future__ import annotations
-from src.core.chat_engine._imports import * # noqa: F401, F403
+import logging
+
+logger = logging.getLogger(__name__)
diff --git a/engine/src/core/chat_engine/_lsp_mixin.py b/engine/src/core/chat_engine/_lsp_mixin.py
index 20bba08..a789408 100644
--- a/engine/src/core/chat_engine/_lsp_mixin.py
+++ b/engine/src/core/chat_engine/_lsp_mixin.py
@@ -2,7 +2,9 @@
"""LSPCommandMixin — LSP 命令处理(拆分自 chat_engine.py)。"""
from __future__ import annotations
-from src.core.chat_engine._imports import * # noqa: F401, F403
+import logging
+
+logger = logging.getLogger(__name__)
diff --git a/engine/src/core/chat_engine/_prompt_mixin.py b/engine/src/core/chat_engine/_prompt_mixin.py
index e03a796..368d3ba 100644
--- a/engine/src/core/chat_engine/_prompt_mixin.py
+++ b/engine/src/core/chat_engine/_prompt_mixin.py
@@ -2,7 +2,18 @@
"""SystemPromptMixin — 系统提示构建与 @文件/@符号 上下文注入(拆分自 chat_engine.py)。"""
from __future__ import annotations
-from src.core.chat_engine._imports import * # noqa: F401, F403
+from src.core.chat_engine._imports import (
+ Any,
+ Dict,
+ List,
+ Path,
+ SystemPromptBuilder,
+ Tuple,
+)
+
+import logging
+
+logger = logging.getLogger(__name__)
diff --git a/engine/src/core/chat_engine/_subagent_mixin.py b/engine/src/core/chat_engine/_subagent_mixin.py
index 661e1b1..757e3ed 100644
--- a/engine/src/core/chat_engine/_subagent_mixin.py
+++ b/engine/src/core/chat_engine/_subagent_mixin.py
@@ -2,7 +2,9 @@
"""SubAgentMixin — 子代理工具接口(拆分自 chat_engine.py)。"""
from __future__ import annotations
-from src.core.chat_engine._imports import * # noqa: F401, F403
+import logging
+
+logger = logging.getLogger(__name__)
diff --git a/engine/src/core/config.py b/engine/src/core/config.py
index c817654..ddc24ce 100644
--- a/engine/src/core/config.py
+++ b/engine/src/core/config.py
@@ -7,6 +7,7 @@
logger = logging.getLogger(__name__)
import io
+import copy
import json
import os
import platform
@@ -229,6 +230,9 @@ def CONFIG_FILE(self):
"allow_model_switch": True, # 允许子代理选择不同模型
"max_tokens_per_subagent": 12000,
},
+ "custom_models": [],
+ "provider_capabilities": {},
+ "context_tokens": 128000,
"username": "User",
"language": "zh", # zh / en / wenyan
"theme": "light", # light / dark
@@ -599,7 +603,7 @@ def _load(self):
# M-008: JSON 反序列化前检查大小,防止超大配置文件导致 OOM
if len(raw_bytes) > MAX_JSON_SIZE:
print(f"[Config] 配置文件超过 {MAX_JSON_SIZE} bytes 大小限制,使用默认配置")
- self.data = dict(self.DEFAULT_CONFIG)
+ self.data = copy.deepcopy(self.DEFAULT_CONFIG)
return
try:
loaded = json.loads(raw_bytes.decode("utf-8"))
@@ -624,15 +628,15 @@ def _load(self):
except json.JSONDecodeError as e:
# 配置文件损坏:回退默认,不覆盖损坏文件
print(f"[Config] 配置文件损坏,使用默认配置: {e}")
- self.data = dict(self.DEFAULT_CONFIG)
+ self.data = copy.deepcopy(self.DEFAULT_CONFIG)
except (RuntimeError, ValueError):
# 安全/校验类异常必须向上抛出,禁止静默回退到默认配置
raise
except Exception as e:
print(f"[Config] 加载失败,使用默认配置: {e}")
- self.data = dict(self.DEFAULT_CONFIG)
+ self.data = copy.deepcopy(self.DEFAULT_CONFIG)
else:
- self.data = dict(self.DEFAULT_CONFIG)
+ self.data = copy.deepcopy(self.DEFAULT_CONFIG)
# 初始化:原子写一次(失败不影响后续写入能力)
try:
self._save()
@@ -851,18 +855,22 @@ def save(self):
self._save()
def get(self, key, default=None):
- """通用 get —— 线程安全:在锁内读取并返回;对 dict/list 类型返回浅副本。"""
+ """Read configuration under its lock, returning detached nested values."""
try:
with self._instance_lock:
+ if key in {"custom_models", "provider_capabilities", "context_tokens"}:
+ provider = self.active_provider
+ if provider:
+ return copy.deepcopy(provider.get(key, self.DEFAULT_CONFIG[key]))
if isinstance(self.data, dict) and key in self.data:
value = self.data[key]
if value is None:
return default
- # 返回不可变类型原样;对 dict/list 返回浅拷贝,避免外部修改污染内部状态
+ # Mutable values must not expose the underlying configuration.
if isinstance(value, dict):
- return dict(value)
+ return copy.deepcopy(value)
if isinstance(value, list):
- return list(value)
+ return copy.deepcopy(value)
return value
except Exception as _exc:
logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
@@ -871,9 +879,12 @@ def get(self, key, default=None):
def set(self, key, value):
"""通用 set —— 线程安全+进程安全:持 instance_lock 更新 data,再通过 _save() 获取跨进程文件锁写入。"""
try:
+ if key in {"model", "api_key", "base_url", "custom_models", "provider_capabilities", "context_tokens", "active_provider_index"}:
+ self.update_settings({key: self._coerce_value(key, value)})
+ return
with self._instance_lock:
if not isinstance(self.data, dict):
- self.data = dict(self.DEFAULT_CONFIG)
+ self.data = copy.deepcopy(self.DEFAULT_CONFIG)
self.data[key] = self._coerce_value(key, value)
# _save() 内部获取跨进程文件锁后重新获取 instance_lock(RLock 可重入)
self._save()
@@ -889,9 +900,12 @@ def set(self, key, value):
def update(self, **kwargs):
"""线程安全+进程安全的批量更新:批量应用 kwargs 后通过 _save() 原子保存。"""
try:
+ if set(kwargs) & {"model", "api_key", "base_url", "custom_models", "provider_capabilities", "context_tokens", "providers", "active_provider_index"}:
+ self.update_settings({key: self._coerce_value(key, value) for key, value in kwargs.items()})
+ return
with self._instance_lock:
if not isinstance(self.data, dict):
- self.data = dict(self.DEFAULT_CONFIG)
+ self.data = copy.deepcopy(self.DEFAULT_CONFIG)
coerced = {k: self._coerce_value(k, v) for k, v in kwargs.items()}
self.data.update(coerced)
# _save() 内部获取跨进程文件锁后重新获取 instance_lock(RLock 可重入)
@@ -905,13 +919,29 @@ def update(self, **kwargs):
except Exception as e:
print(f"[Config] update 失败: {e}")
+ def update_settings(self, values: Dict[str, Any]) -> None:
+ """Persist a validated UI batch once, synchronizing active provider fields."""
+ with self._instance_lock:
+ previous = copy.deepcopy(self.data)
+ try:
+ self.data.update(copy.deepcopy(values))
+ if "nickname" in values:
+ self.data["username"] = values["nickname"]
+ providers = self.data.get("providers", [])
+ if providers:
+ index = self.active_provider_index
+ provider = providers[index if 0 <= index < len(providers) else 0]
+ for key in ("model", "base_url", "api_key", "custom_models", "provider_capabilities", "context_tokens"):
+ if key in values:
+ provider[key] = copy.deepcopy(values[key])
+ self._save()
+ except Exception:
+ self.data = previous
+ raise
+
def get_api_key(self) -> str:
- """获取 API Key(内部解密)。线程安全。"""
- try:
- with self._instance_lock:
- return self.data.get("api_key", "")
- except Exception:
- return ""
+ """Use the same credential selection as model requests."""
+ return self.api_key
@property
def active_provider(self) -> dict:
@@ -920,13 +950,11 @@ def active_provider(self) -> dict:
providers = self.data.get("providers")
if not isinstance(providers, list) or not providers:
return {}
- idx = self.data.get("active_provider_index", 0)
- if not isinstance(idx, int) or idx < 0 or idx >= len(providers):
- idx = 0
+ idx = self.active_provider_index
p = providers[idx]
if not isinstance(p, dict):
return {}
- return dict(p)
+ return copy.deepcopy(p)
@property
def active_provider_index(self) -> int:
@@ -937,14 +965,12 @@ def active_provider_index(self) -> int:
providers = self.data.get("providers", [])
if isinstance(providers, list) and idx >= len(providers):
idx = max(0, len(providers) - 1)
- return idx
+ return max(0, idx)
@active_provider_index.setter
def active_provider_index(self, value):
value = self._coerce_value("active_provider_index", value)
- with self._instance_lock:
- self.data["active_provider_index"] = value
- self._save_safe()
+ self.update_settings({"active_provider_index": value})
# 快捷属性:全部线程安全,读/写持锁
@property
@@ -955,15 +981,13 @@ def api_key(self):
if env_key:
return env_key
active = self.active_provider
- if active.get("api_key"):
+ if "api_key" in active:
return active["api_key"]
return self.data.get("api_key", "")
@api_key.setter
def api_key(self, value):
- with self._instance_lock:
- self.data["api_key"] = value
- self._save_safe()
+ self.update_settings({"api_key": str(value or "")})
@property
def model(self):
@@ -975,9 +999,7 @@ def model(self):
@model.setter
def model(self, value):
- with self._instance_lock:
- self.data["model"] = str(value or "deepseek-v4-pro")
- self._save_safe()
+ self.update_settings({"model": str(value or "deepseek-v4-pro")})
@property
def base_url(self) -> str:
@@ -989,9 +1011,7 @@ def base_url(self) -> str:
@base_url.setter
def base_url(self, value):
- with self._instance_lock:
- self.data["base_url"] = str(value or "https://api.deepseek.com")
- self._save_safe()
+ self.update_settings({"base_url": str(value or "https://api.deepseek.com")})
@property
def language(self):
@@ -1144,4 +1164,4 @@ def get_model_info(self, model_name: str) -> dict:
for m in self.get_available_models():
if isinstance(m, dict) and m.get("name") == model_name:
return dict(m)
- return {"name": model_name, "label": model_name, "description": "", "primary": False}
\ No newline at end of file
+ return {"name": model_name, "label": model_name, "description": "", "primary": False}
diff --git a/engine/src/core/context_budget.py b/engine/src/core/context_budget.py
new file mode 100644
index 0000000..021d97c
--- /dev/null
+++ b/engine/src/core/context_budget.py
@@ -0,0 +1,64 @@
+"""Build bounded requests without splitting native tool exchanges or changing history."""
+
+import json
+
+
+def estimated_tokens(value):
+ # A conservative heuristic, not a provider tokenizer. Images have a separate allowance.
+ if isinstance(value, list):
+ return sum(4096 if isinstance(part, dict) and part.get("type") == "image_url"
+ else estimated_tokens(part) for part in value)
+ if isinstance(value, dict):
+ if value.get("type") == "image_url": return 4096
+ return 4 + sum(estimated_tokens(k) + estimated_tokens(v) for k, v in value.items())
+ text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
+ return max(1, (len(text.encode("utf-8")) + 1) // 2)
+
+
+def complete_tool_exchanges(messages):
+ result, pending = [], {}
+ def finish_pending():
+ for call_id in pending:
+ result.append({"role": "tool", "tool_call_id": call_id,
+ "content": "工具调用未完成或已取消;没有可用执行结果。"})
+ pending.clear()
+ for source in messages:
+ message = dict(source)
+ if message.get("role") == "tool":
+ call_id = message.get("tool_call_id")
+ if call_id in pending:
+ pending.pop(call_id)
+ else:
+ message = {"role": "user", "content": "[历史工具结果]\n" + str(message.get("content", ""))}
+ finish_pending()
+ else:
+ finish_pending()
+ for call in message.get("tool_calls", []):
+ pending[call["id"]] = True
+ result.append(message)
+ finish_pending()
+ return result
+
+
+def fit_context(messages, limit, tools=None):
+ prepared = complete_tool_exchanges(messages)
+ prefix = prepared[:1] if prepared and prepared[0].get("role") == "system" else []
+ groups = []
+ for message in prepared[len(prefix):]:
+ if not groups or message.get("role") == "user":
+ groups.append([])
+ groups[-1].append(message)
+ reserve = min(4096, max(128, limit // 8))
+ available = limit - reserve - estimated_tokens(tools or []) - estimated_tokens(prefix)
+ if not groups:
+ if available < 0: raise ValueError("系统提示与工具描述超过上下文预算")
+ return prepared, 0
+ costs = [estimated_tokens(group) for group in groups]
+ if costs[-1] > available:
+ raise ValueError("当前消息、系统提示与工具描述超过上下文预算;请减少附件或提高 context_tokens")
+ selected = len(groups) - 1
+ remaining = available - costs[-1]
+ while selected > 0 and costs[selected - 1] <= remaining:
+ selected -= 1
+ remaining -= costs[selected]
+ return prefix + [m for group in groups[selected:] for m in group], sum(len(g) for g in groups[:selected])
diff --git a/engine/src/core/deepseek_api.py b/engine/src/core/deepseek_api.py
index 5652289..f7f68ba 100644
--- a/engine/src/core/deepseek_api.py
+++ b/engine/src/core/deepseek_api.py
@@ -15,8 +15,10 @@
logger = logging.getLogger(__name__)
import json
+import os
import re
import socket
+import ssl
import threading
import time
from contextlib import contextmanager
@@ -106,6 +108,8 @@ def _get_ssl_verify(self) -> Any:
当 certifi 不可用时显式记录 warning,而不是静默 fallback 到系统默认 CA(M-004)。
"""
+ if os.environ.get("SSL_CERT_FILE"):
+ return os.environ["SSL_CERT_FILE"]
if certifi is not None:
return certifi.where()
logger.warning(
@@ -135,14 +139,18 @@ def _get_http_session(self) -> Optional[Any]:
self._http_session = None
if httpx is None:
return None
- timeout = self._make_httpx_timeout()
- self._http_session = httpx.Client(
- verify=self._get_ssl_verify(),
- timeout=timeout,
- )
+ self._http_session = self._make_http_client()
self._http_session_key = key
return self._http_session
+ def _make_http_client(self):
+ if httpx is None:
+ return None
+ verify = self._get_ssl_verify()
+ if isinstance(verify, str):
+ verify = ssl.create_default_context(cafile=verify)
+ return httpx.Client(verify=verify, timeout=self._make_httpx_timeout())
+
def _get_client(self) -> Optional["OpenAI"]:
_ensure_openai()
with self._session_lock:
@@ -157,11 +165,13 @@ def _get_client(self) -> Optional["OpenAI"]:
# 使用 httpx.Timeout 设置连接/读取超时,避免弱网/服务端无响应时永久阻塞
timeout = self._make_httpx_timeout()
# 显式传入 http_client 并强制开启 SSL 证书验证(CWE-295 / MEDIUM-001)
- http_client = self._get_http_session()
+ # Each OpenAI client owns its transport. Closing an old client must
+ # never close the diagnostics session or its replacement client.
+ http_client = self._make_http_client()
old_client = self._client
self._client = OpenAI(
api_key=key, base_url=base_url,
- timeout=timeout,
+ timeout=timeout, max_retries=0,
http_client=http_client,
)
self._client_key = key
@@ -301,9 +311,7 @@ def check_balance(self) -> Dict:
data = resp.json()
except (ValueError, json.JSONDecodeError):
return {"available_balance": 0, "ok": False, "error": "bad_json"}
- avail = float(data.get("available_balance", 0) or 0)
- total = float(data.get("total_balance", 0) or 0)
- return {"available_balance": avail, "total_balance": total, "ok": True}
+ return self._normalize_balance(data)
return {
"available_balance": 0, "ok": False,
"code": resp.status_code,
@@ -334,9 +342,7 @@ def check_balance(self) -> Dict:
data = resp.json()
except (ValueError, json.JSONDecodeError):
return {"available_balance": 0, "ok": False, "error": "bad_json"}
- avail = float(data.get("available_balance", 0) or 0)
- total = float(data.get("total_balance", 0) or 0)
- return {"available_balance": avail, "total_balance": total, "ok": True}
+ return self._normalize_balance(data)
return {
"available_balance": 0, "ok": False,
"code": resp.status_code,
@@ -351,6 +357,39 @@ def check_balance(self) -> Dict:
except Exception as _exc:
logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
+ @staticmethod
+ def _normalize_balance(data: Dict) -> Dict:
+ infos = data.get("balance_infos", [])
+ if infos:
+ selected = next((i for i in infos if i.get("currency") == "CNY"), infos[0])
+ total = float(selected.get("total_balance", 0) or 0)
+ return {"available_balance": total, "total_balance": total, "ok": True,
+ "currency": selected.get("currency", ""), "balance_infos": infos,
+ "is_available": bool(data.get("is_available", False))}
+ return {"available_balance": float(data.get("available_balance", 0) or 0),
+ "total_balance": float(data.get("total_balance", 0) or 0), "ok": True,
+ "currency": data.get("currency", "CNY")}
+
+ def list_models(self) -> Dict:
+ """Fetch models using the configured URL, port, path and TLS policy."""
+ if not self.cfg.api_key:
+ return {"ok": False, "models": [], "error": "未配置 API Key"}
+ try:
+ session = self._get_http_session()
+ if session is None:
+ return {"ok": False, "models": [], "error": "请安装 requirements.txt 中的依赖"}
+ resp = session.get(f"{self._base_url()}/models",
+ headers={"Authorization": f"Bearer {self.cfg.api_key}"})
+ try:
+ resp.raise_for_status()
+ return {"ok": True, "models": [
+ {"id": m["id"], "name": m["id"], "label": m["id"]}
+ for m in resp.json().get("data", []) if m.get("id")]}
+ finally:
+ resp.close()
+ except Exception as exc:
+ return {"ok": False, "models": [], "error": self._sanitize_error_message(str(exc))}
+
def _balance_error_response(self, e) -> Dict:
"""统一余额查询异常返回格式(detail 经过脱敏,避免泄漏 API Key)。"""
detail = self._sanitize_error_message(str(e), max_length=200)
@@ -487,6 +526,30 @@ def chat_stream(self, messages: List[Dict], model: Optional[str] = None,
if last_exception is not None:
raise last_exception
+ def _request_kwargs(self, model, messages, reasoning_effort, enable_thinking):
+ from urllib.parse import urlsplit
+ capabilities = self.cfg.get("provider_capabilities", {}) or {}
+ deepseek = urlsplit(self._base_url()).hostname == "api.deepseek.com" or model.startswith("deepseek-")
+ kwargs = dict(model=model, messages=messages, stream=True)
+ if capabilities.get("stream_usage", True):
+ kwargs["stream_options"] = {"include_usage": True}
+ if enable_thinking and capabilities.get("reasoning_effort", deepseek):
+ kwargs["reasoning_effort"] = reasoning_effort
+ if enable_thinking and capabilities.get("thinking", deepseek):
+ kwargs["extra_body"] = {"thinking": {"type": "enabled"}}
+ if capabilities.get("tools", True):
+ from src.core.prompts.tool_descriptions import TOOL_DESCRIPTIONS
+ kwargs["tools"] = self._to_openai_tools(TOOL_DESCRIPTIONS)
+ from src.core.context_budget import fit_context
+ try:
+ limit = int(self.cfg.get("context_tokens", 128000))
+ kwargs["messages"], removed = fit_context(messages, limit, kwargs.get("tools"))
+ except (ValueError, TypeError) as exc:
+ raise APIError(400, str(exc), need_retry=False) from exc
+ if removed:
+ logger.info("上下文预算省略 %s 条旧消息;磁盘历史保持完整", removed)
+ return kwargs
+
def _chat_stream_once(self, client: "OpenAI", use_model: str, messages: List[Dict],
reasoning_effort: str,
on_text: Optional[Callable[[str], None]] = None,
@@ -495,43 +558,28 @@ def _chat_stream_once(self, client: "OpenAI", use_model: str, messages: List[Dic
enable_thinking: bool = True) -> Dict:
"""单次流式对话尝试(内部方法)。"""
_ensure_openai()
- full_content = ""
- full_reasoning = ""
- usage_summary = {}
- stream = None # 避免 finally 中 UnboundLocalError
-
- # 取消状态容器 (闭包可用, 同时也用 on_cancel_check)
- _request_cancelled = [False]
+ stream = None
try:
# C-006: 用 socket 默认超时把 DNS 解析包含在连接超时内;
# stream 对象返回后已建立连接,随即恢复旧超时。
with _socket_dns_timeout(self._HTTP_TIMEOUT_CONNECT):
# 构建请求参数 — 思考模式 vs 非思考模式
- create_kwargs = dict(
- model=use_model,
- messages=messages,
- stream=True,
- stream_options={"include_usage": True},
- )
- if enable_thinking:
- create_kwargs["reasoning_effort"] = reasoning_effort
- create_kwargs["extra_body"] = {"thinking": {"type": "enabled"}}
- # 注入 OpenAI 原生 tools 定义,启用 DeepSeek V4 原生 function calling
- try:
- from src.core.prompts.tool_descriptions import TOOL_DESCRIPTIONS
- if TOOL_DESCRIPTIONS:
- create_kwargs["tools"] = DeepSeekAPI._to_openai_tools(TOOL_DESCRIPTIONS)
- except Exception:
- pass
+ create_kwargs = self._request_kwargs(use_model, messages, reasoning_effort, enable_thinking)
stream = client.chat.completions.create(**create_kwargs)
- except _OPENAI_ERRORS as e:
- code, msg = self._parse_error(e)
- raise APIError(code, msg)
+ except APIError:
+ raise
except Exception as e:
code, msg = self._parse_error(e)
- raise APIError(code, msg)
+ raise APIError(code, msg) from e
+
+ return self._consume_stream(stream, use_model, on_text, on_reasoning, on_cancel_check)
+ def _consume_stream(self, stream, use_model, on_text=None, on_reasoning=None, on_cancel_check=None):
+ full_content = ""
+ full_reasoning = ""
+ usage_summary = {}
+ _request_cancelled = [False]
try:
for chunk in stream:
# 每次迭代前检查取消
@@ -548,14 +596,13 @@ def _chat_stream_once(self, client: "OpenAI", use_model: str, messages: List[Dic
# usage chunk: choices 为空
if hasattr(chunk, "usage") and chunk.usage:
u = chunk.usage
- usage_summary = {
+ usage_summary.update({
"prompt_tokens": getattr(u, "prompt_tokens", 0),
"completion_tokens": getattr(u, "completion_tokens", 0),
"total_tokens": getattr(u, "total_tokens", 0),
"prompt_cache_hit_tokens": getattr(u, "prompt_cache_hit_tokens", 0) or 0,
"prompt_cache_miss_tokens": getattr(u, "prompt_cache_miss_tokens", 0) or 0,
- }
- continue
+ })
if not chunk.choices:
continue
delta = chunk.choices[0].delta
@@ -575,6 +622,10 @@ def _chat_stream_once(self, client: "OpenAI", use_model: str, messages: List[Dic
existing = [t for t in usage_summary["tool_calls"] if t.get("index") == idx]
if existing:
existing[0]["function"]["arguments"] += fn_args
+ if fn_name:
+ existing[0]["function"]["name"] += fn_name
+ if getattr(tc, "id", None):
+ existing[0]["id"] = tc.id
else:
usage_summary["tool_calls"].append({
"index": idx,
@@ -598,10 +649,10 @@ def _chat_stream_once(self, client: "OpenAI", use_model: str, messages: List[Dic
logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
except _OPENAI_ERRORS as e:
code, msg = self._parse_error(e)
- raise APIError(code, msg)
+ raise APIError(code, msg, need_retry=not (full_content or full_reasoning or usage_summary.get("tool_calls"))) from e
except Exception as e:
code, msg = self._parse_error(e)
- raise APIError(code, msg)
+ raise APIError(code, msg, need_retry=not (full_content or full_reasoning or usage_summary.get("tool_calls"))) from e
finally:
try:
if stream is not None:
@@ -634,209 +685,16 @@ def chat_stream_with_images(self, messages: List[Dict], model: Optional[str] = N
thinking + reasoning_effort 同样会传递。
"""
- # P0-001: 防止图片降级无限递归,超过 2 次重试直接抛错
- if _vision_retry_count >= 2:
- raise APIError(500, "图片理解降级失败,无法继续")
-
- client = self._get_client()
- if client is None:
- raise APIError(401, "请先在设置中配置 API Key")
-
- use_model = model or self.cfg.model
- _reasoning_effort = reasoning_effort or self.cfg.get("reasoning_effort", "high")
-
imgs = list(image_data_urls or [])
-
- # 没有图片时,直接走普通 chat_stream
- if not imgs:
- return self.chat_stream(
- messages, model=use_model,
- on_text=on_text, on_reasoning=on_reasoning,
- on_cancel_check=on_cancel_check,
- reasoning_effort=_reasoning_effort,
- enable_thinking=enable_thinking,
- )
-
- # 构造 multi-modal messages:找到最后一条 user 消息,把图片附加在其前面(同一条)
- try:
- multimodal_messages = self._build_vision_messages(messages, imgs)
- except Exception:
- multimodal_messages = list(messages)
-
- full_content = ""
- full_reasoning = ""
- usage_summary = {}
- stream = None # 避免 finally 中 UnboundLocalError
-
- # 取消状态容器
- _request_cancelled = [False]
-
- try:
- # C-006: 用 socket 默认超时把 DNS 解析包含在连接超时内
- with _socket_dns_timeout(self._HTTP_TIMEOUT_CONNECT):
- create_kwargs = dict(
- model=use_model,
- messages=multimodal_messages,
- stream=True,
- stream_options={"include_usage": True},
- )
- if enable_thinking:
- create_kwargs["reasoning_effort"] = _reasoning_effort
- create_kwargs["extra_body"] = {"thinking": {"type": "enabled"}}
- stream = client.chat.completions.create(**create_kwargs)
- except _OPENAI_ERRORS as e:
- msg_text = str(e).lower()
- # C-001: 仅非降级调用且异常明确与 vision 相关时才降级
- if not _is_degraded_call and any(k in msg_text for k in ("image", "vision", "multimodal", "does not support image", "images are not supported", "image input", "unsupported image", "unsupported vision", "unsupported multimodal")):
- return self.chat_stream(
- messages, model=use_model,
- on_text=on_text, on_reasoning=on_reasoning,
- on_cancel_check=on_cancel_check,
- reasoning_effort=_reasoning_effort,
- enable_thinking=enable_thinking,
- _vision_retry_count=_vision_retry_count + 1,
- _is_degraded_call=True,
- )
- code, msg = self._parse_error(e)
- raise APIError(code, msg)
- except Exception as e:
- # C-001: 非 vision 相关异常直接转换为 APIError,不再尝试降级
- msg_text = str(e).lower()
- if not _is_degraded_call and any(k in msg_text for k in ("image", "vision", "multimodal", "does not support image", "images are not supported", "image input", "unsupported image", "unsupported vision", "unsupported multimodal")):
- try:
- return self.chat_stream(
- messages, model=use_model,
- on_text=on_text, on_reasoning=on_reasoning,
- on_cancel_check=on_cancel_check,
- reasoning_effort=_reasoning_effort,
- enable_thinking=enable_thinking,
- _vision_retry_count=_vision_retry_count + 1,
- _is_degraded_call=True,
- )
- except APIError:
- raise
- except Exception as e2:
- code, msg = self._parse_error(e2)
- raise APIError(code, msg)
- code, msg = self._parse_error(e)
- raise APIError(code, msg)
-
- try:
- vision_failed = False
- for chunk in stream:
- if on_cancel_check:
- try:
- if bool(on_cancel_check()):
- _request_cancelled[0] = True
- break
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
- if _request_cancelled[0]:
- break
-
- if hasattr(chunk, "usage") and chunk.usage:
- u = chunk.usage
- usage_summary = {
- "prompt_tokens": getattr(u, "prompt_tokens", 0),
- "completion_tokens": getattr(u, "completion_tokens", 0),
- "total_tokens": getattr(u, "total_tokens", 0),
- "prompt_cache_hit_tokens": getattr(u, "prompt_cache_hit_tokens", 0) or 0,
- "prompt_cache_miss_tokens": getattr(u, "prompt_cache_miss_tokens", 0) or 0,
- }
- continue
-
- if not chunk.choices:
- continue
-
- delta = chunk.choices[0].delta
- reasoning = getattr(delta, "reasoning_content", None) \
- or getattr(delta, "reasoning", None) or ""
- text = getattr(delta, "content", None) or ""
-
- # 兼容一些服务端把不支持的字段放在 error 里
- if not text and not reasoning:
- maybe_err = getattr(delta, "error", None) or getattr(chunk, "error", None)
- if maybe_err:
- err_str = str(maybe_err).lower()
- if any(k in err_str for k in ("image", "vision", "multimodal", "does not support image", "images are not supported", "image input", "unsupported image", "unsupported vision", "unsupported multimodal")):
- vision_failed = True
- break
-
- if reasoning:
- full_reasoning += reasoning
- if on_reasoning:
- try:
- on_reasoning(reasoning)
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
- if text:
- full_content += text
- if on_text:
- try:
- on_text(text)
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
-
- if vision_failed:
- # 流迭代中途已产生部分内容;若重新调用 chat_stream 会从头
- # 再次推送,导致 GUI 重复渲染。直接返回当前已收集结果并标
- # 记为 vision 降级(B-03)。
- usage_summary["content"] = full_content
- usage_summary["reasoning_content"] = full_reasoning
- usage_summary["cancelled"] = _request_cancelled[0]
- usage_summary["used_vision"] = True
- usage_summary["vision_degraded"] = True
- usage_summary.update(self._calc_cost(usage_summary, use_model))
- return usage_summary
- except _OPENAI_ERRORS as e:
- msg_text = str(e).lower()
- # C-001: 仅非降级调用且异常明确与 vision 相关时才降级
- if not _is_degraded_call and any(k in msg_text for k in ("image", "vision", "multimodal", "does not support image", "images are not supported", "image input", "unsupported image", "unsupported vision", "unsupported multimodal")):
- return self.chat_stream(
- messages, model=use_model,
- on_text=on_text, on_reasoning=on_reasoning,
- on_cancel_check=on_cancel_check,
- reasoning_effort=_reasoning_effort,
- enable_thinking=enable_thinking,
- _vision_retry_count=_vision_retry_count + 1,
- _is_degraded_call=True,
- )
- code, msg = self._parse_error(e)
- raise APIError(code, msg)
- except Exception as e:
- # C-001: 非 vision 相关异常直接转换为 APIError,不再尝试降级
- msg_text = str(e).lower()
- if not _is_degraded_call and any(k in msg_text for k in ("image", "vision", "multimodal", "does not support image", "images are not supported", "image input", "unsupported image", "unsupported vision", "unsupported multimodal")):
- try:
- return self.chat_stream(
- messages, model=use_model,
- on_text=on_text, on_reasoning=on_reasoning,
- on_cancel_check=on_cancel_check,
- reasoning_effort=_reasoning_effort,
- enable_thinking=enable_thinking,
- _vision_retry_count=_vision_retry_count + 1,
- _is_degraded_call=True,
- )
- except APIError:
- raise
- except Exception as e2:
- code, msg = self._parse_error(e2)
- raise APIError(code, msg)
- code, msg = self._parse_error(e)
- raise APIError(code, msg)
- finally:
- try:
- if stream is not None:
- stream.close()
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
-
- usage_summary["content"] = full_content
- usage_summary["reasoning_content"] = full_reasoning
- usage_summary["cancelled"] = _request_cancelled[0]
- usage_summary["used_vision"] = True
- usage_summary.update(self._calc_cost(usage_summary, use_model))
- return usage_summary
+ capabilities = self.cfg.get("provider_capabilities", {}) or {}
+ if imgs and capabilities.get("vision") is False:
+ raise APIError(400, "当前供应商已禁用图片输入,请选择支持图片的模型", need_retry=False)
+ prepared = self._build_vision_messages(messages, imgs) if imgs else messages
+ result = self.chat_stream(prepared, model=model, on_text=on_text,
+ on_reasoning=on_reasoning, on_cancel_check=on_cancel_check,
+ reasoning_effort=reasoning_effort, enable_thinking=enable_thinking)
+ result["used_vision"] = bool(imgs)
+ return result
# ----- 工具: 把最后一条用户消息转为多模态格式 -----
def _build_vision_messages(self, messages: List[Dict], image_data_urls: List[str]) -> List[Dict]:
diff --git a/engine/src/core/hooks.py b/engine/src/core/hooks.py
index c2acb7e..60d10bf 100644
--- a/engine/src/core/hooks.py
+++ b/engine/src/core/hooks.py
@@ -461,7 +461,6 @@ def trigger_blocking(self, event: str, **kwargs) -> Tuple[bool, str, str]:
for r in results:
if r.error:
logger.debug("钩子 %s 执行错误: %s", r.hook_name, r.error)
- continue
if r.blocked:
blocked = True
reason = f"[{r.hook_name}] {r.context_injection or '被钩子阻断'}"
@@ -489,7 +488,7 @@ def _execute_hook(self, hook: Hook, **kwargs) -> HookResult:
def _build_env(self, hook: Hook, **kwargs) -> Dict[str, str]:
"""构建钩子执行的环境变量"""
- env = os.environ.copy()
+ env = {k: v for k, v in os.environ.items() if k != "DSCODE_SIDECAR_TOKEN"}
env["DSCODE_HOOK_EVENT"] = hook.event
env["DSCODE_HOOK_NAME"] = hook.name
env["DSCODE_WORK_DIR"] = str(self.work_dir) if self.work_dir else ""
diff --git a/engine/src/core/lsp_client.py b/engine/src/core/lsp_client.py
index bc2f9af..f2ac0ab 100644
--- a/engine/src/core/lsp_client.py
+++ b/engine/src/core/lsp_client.py
@@ -56,7 +56,7 @@ def __init__(self, root_uri: str, workspace_folders: Optional[List[str]] = None)
root_uri: 项目根URI,如 "file:///home/user/project"
"""
self.root_uri = root_uri.rstrip("/") if root_uri else ""
- self.root_path = root_uri.replace("file://", "", 1) if root_uri.startswith("file://") else root_uri
+ self.root_path = self.uri_to_path(root_uri)
self.workspace_folders = workspace_folders or [{"uri": self.root_uri, "name": Path(self.root_path).name}]
self._proc: Optional[subprocess.Popen] = None
self._msg_id = 0
@@ -77,16 +77,20 @@ def __init__(self, root_uri: str, workspace_folders: Optional[List[str]] = None)
def from_path(cls, path: str) -> "LSPClient":
"""从文件路径创建LSPClient"""
abs_path = str(Path(path).resolve())
- return cls(f"file://{abs_path}")
+ return cls(cls.path_to_uri(abs_path))
@staticmethod
def path_to_uri(path: str) -> str:
- return "file://" + str(Path(path).resolve())
+ return Path(path).resolve().as_uri()
@staticmethod
def uri_to_path(uri: str) -> str:
if uri.startswith("file://"):
- return uri[7:]
+ from urllib.parse import urlsplit
+ from urllib.request import url2pathname
+ parsed = urlsplit(uri)
+ path = url2pathname(parsed.path)
+ return "//" + parsed.netloc + path if parsed.netloc else path
return uri
# ── 生命周期 ──────────────────────────────────────────────
@@ -608,7 +612,7 @@ def _stderr_drain_loop(self):
while not self._shutdown_requested:
if self._proc is None or self._proc.stderr is None:
break
- chunk = self._proc.stderr.read(4096)
+ chunk = self._proc.stderr.read1(4096)
if not chunk:
break
# 只记录warning以上的stderr,避免日志泛滥
@@ -631,7 +635,7 @@ def _reader_loop(self):
if self._proc is None or self._proc.stdout is None:
break
# 大块读取,避免逐字节read的性能问题
- chunk = self._proc.stdout.read(4096)
+ chunk = self._proc.stdout.read1(4096)
if not chunk:
# stdout关闭,服务器退出
break
@@ -657,7 +661,7 @@ def _reader_loop(self):
content_length = int(line.split(":", 1)[1].strip())
except ValueError:
content_length = -1
- if content_length is None or content_length < 0:
+ if content_length is None or content_length < 0 or content_length > 16 * 1024 * 1024:
# 没有Content-Length或解析失败,流可能错位,断开连接
logger.warning("LSP %s: invalid/missing Content-Length header, disconnecting",
self._server_name)
diff --git a/engine/src/core/memory_manager.py b/engine/src/core/memory_manager.py
index ea3b10a..4b192d5 100644
--- a/engine/src/core/memory_manager.py
+++ b/engine/src/core/memory_manager.py
@@ -18,6 +18,7 @@
import hashlib
import json
+import copy
import threading
from datetime import datetime, timedelta, timezone
from functools import lru_cache
@@ -333,7 +334,7 @@ def update(self, mem_id: str, **fields: Any) -> bool:
with self._lock:
self._ensure_loaded()
self._invalidate_search_cache()
- item = self._items.get(mem_id)
+ item = copy.deepcopy(self._items.get(mem_id))
if item is None:
return False
changed = False
@@ -363,6 +364,7 @@ def update(self, mem_id: str, **fields: Any) -> bool:
if changed:
item["updated_at"] = _now_iso()
if self._write_json(self._item_path(mem_id), item):
+ self._items[mem_id] = item
self.save()
return True
return False
@@ -375,13 +377,14 @@ def delete(self, mem_id: str) -> bool:
self._invalidate_search_cache()
if mem_id not in self._items:
return False
- self._items.pop(mem_id, None)
path = self._item_path(mem_id)
try:
if path.exists():
path.unlink()
except OSError as exc:
print(f"[MemoryManager] 删除 {path.name} 失败: {exc}")
+ return False
+ self._items.pop(mem_id, None)
self.save()
return True
@@ -393,12 +396,12 @@ def get(self, mem_id: str) -> Optional[Dict[str, Any]]:
with self._lock:
self._ensure_loaded()
item = self._items.get(mem_id)
- return dict(item) if item is not None else None
+ return copy.deepcopy(item) if item is not None else None
def list_all(self) -> List[Dict[str, Any]]:
with self._lock:
self._ensure_loaded()
- items = [dict(it) for it in self._items.values()]
+ items = [copy.deepcopy(it) for it in self._items.values()]
items.sort(key=lambda it: it.get("updated_at", ""), reverse=True)
return items
@@ -418,7 +421,7 @@ def search(
cache_hash = self._hash_search_key(key)
if self._search_cache_hash == cache_hash and self._search_cache_result is not None:
- return [dict(it) for it in self._search_cache_result]
+ return copy.deepcopy(self._search_cache_result)
results: List[Dict[str, Any]] = []
for item in self._items.values():
@@ -432,7 +435,7 @@ def search(
haystack = f"{item.get('title', '')}\n{item.get('content', '')}".lower()
if kw not in haystack:
continue
- results.append(dict(item))
+ results.append(copy.deepcopy(item))
results.sort(key=lambda it: it.get("updated_at", ""), reverse=True)
results = results[: max(0, int(limit))]
diff --git a/engine/src/core/prompts/tool_descriptions.json b/engine/src/core/prompts/tool_descriptions.json
index f1274a2..6364fc8 100644
--- a/engine/src/core/prompts/tool_descriptions.json
+++ b/engine/src/core/prompts/tool_descriptions.json
@@ -1607,5 +1607,289 @@
}
],
"returns": "(ok, message). Updates the conversation display name in the sidebar."
+ },
+ {
+ "name": "git_add",
+ "description": "Stage project files.",
+ "fields": [
+ {
+ "key": "files",
+ "type": "array",
+ "required": true
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "git_branch_create",
+ "description": "Create a project Git branch.",
+ "fields": [
+ {
+ "key": "branch",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "base",
+ "type": "string",
+ "required": false
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "git_branch_delete",
+ "description": "Delete a Git branch; reviewer and read-only agents cannot use this.",
+ "fields": [
+ {
+ "key": "branch",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "force",
+ "type": "boolean",
+ "required": false
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "git_reset",
+ "description": "Reset Git index/worktree. Choose soft, mixed or hard explicitly.",
+ "fields": [
+ {
+ "key": "mode",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "target",
+ "type": "string",
+ "required": false
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "git_stash",
+ "description": "Save or restore worktree changes using Git stash.",
+ "fields": [
+ {
+ "key": "action",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "message",
+ "type": "string",
+ "required": false
+ },
+ {
+ "key": "index",
+ "type": "integer",
+ "required": false
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "replace_symbol_in_files",
+ "description": "Replace a symbol using the sandbox text fallback.",
+ "fields": [
+ {
+ "key": "old_name",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "new_name",
+ "type": "string",
+ "required": true
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "shell_start",
+ "description": "Start an owned persistent shell inside the project.",
+ "fields": [],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "shell_exec",
+ "description": "Run a command in the persistent shell and wait for its exit status.",
+ "fields": [
+ {
+ "key": "command",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "timeout",
+ "type": "number",
+ "required": false
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "shell_send",
+ "description": "Send a command to the persistent shell without waiting.",
+ "fields": [
+ {
+ "key": "command",
+ "type": "string",
+ "required": true
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "shell_read",
+ "description": "Read buffered persistent shell output.",
+ "fields": [
+ {
+ "key": "clear",
+ "type": "boolean",
+ "required": false
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "shell_cd",
+ "description": "Change the persistent shell directory inside the project.",
+ "fields": [
+ {
+ "key": "path",
+ "type": "string",
+ "required": true
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "shell_stop",
+ "description": "Stop the owned persistent shell and its child processes.",
+ "fields": [],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "goto_definition",
+ "description": "Resolve a symbol definition using LSP or local fallback.",
+ "fields": [
+ {
+ "key": "file",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "line",
+ "type": "integer",
+ "required": true
+ },
+ {
+ "key": "character",
+ "type": "integer",
+ "required": true
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "find_references",
+ "description": "Find references to a symbol; positions are one-based.",
+ "fields": [
+ {
+ "key": "file",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "line",
+ "type": "integer",
+ "required": true
+ },
+ {
+ "key": "character",
+ "type": "integer",
+ "required": true
+ },
+ {
+ "key": "include_declaration",
+ "type": "boolean",
+ "required": false
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "hover_info",
+ "description": "Get documentation/type information for a symbol.",
+ "fields": [
+ {
+ "key": "file",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "line",
+ "type": "integer",
+ "required": true
+ },
+ {
+ "key": "character",
+ "type": "integer",
+ "required": true
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "get_diagnostics",
+ "description": "Read LSP diagnostics for a file.",
+ "fields": [
+ {
+ "key": "file",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "severity",
+ "type": "string",
+ "required": false
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "document_symbols",
+ "description": "List symbols in a document.",
+ "fields": [
+ {
+ "key": "file",
+ "type": "string",
+ "required": true
+ }
+ ],
+ "returns": "(ok, result text)"
+ },
+ {
+ "name": "format_document",
+ "description": "Format a document using LSP or local fallback.",
+ "fields": [
+ {
+ "key": "file",
+ "type": "string",
+ "required": true
+ },
+ {
+ "key": "apply",
+ "type": "boolean",
+ "required": false
+ }
+ ],
+ "returns": "(ok, result text)"
}
-]
\ No newline at end of file
+]
diff --git a/engine/src/gui/animations/_shadow.py b/engine/src/gui/animations/_shadow.py
index fd13094..e3708dc 100644
--- a/engine/src/gui/animations/_shadow.py
+++ b/engine/src/gui/animations/_shadow.py
@@ -9,7 +9,7 @@
from PySide6.QtCore import QPropertyAnimation
from PySide6.QtGui import QColor
-from PySide6.QtWidgets import QWidget, QGraphicsDropShadowEffect
+from PySide6.QtWidgets import QWidget, QGraphicsDropShadowEffect, QGraphicsBlurEffect
from src.gui.style import COLORS
from src.gui.animations._constants import EASE_SPRING, DURATION_PANEL
diff --git a/engine/src/gui/animations/_widgets.py b/engine/src/gui/animations/_widgets.py
index e5a77fe..fca050f 100644
--- a/engine/src/gui/animations/_widgets.py
+++ b/engine/src/gui/animations/_widgets.py
@@ -14,7 +14,7 @@
from typing import Dict, Optional, Tuple
from PySide6.QtCore import (
- QPropertyAnimation, QEasingCurve, QTimer, Property, Qt, Signal,
+ QPropertyAnimation, QEasingCurve, QTimer, Property, Qt, Signal, QEvent,
)
from PySide6.QtWidgets import (
QWidget, QPushButton, QFrame,
diff --git a/engine/src/gui/bubbles/animation_icons.py b/engine/src/gui/bubbles/animation_icons.py
index 3b3780e..f830d47 100644
--- a/engine/src/gui/bubbles/animation_icons.py
+++ b/engine/src/gui/bubbles/animation_icons.py
@@ -13,7 +13,7 @@
from PySide6.QtWidgets import QWidget, QFrame
from src.gui.style import RADII, COLORS, _cached_style
-from src.gui.icon_factory import get_icon, draw_keyboard, draw_terminal, draw_paper
+from src.gui.icon_factory import get_icon
from src.gui.animations import pop_in
from src.gui.bubbles._helpers import _img, _try_pm
diff --git a/engine/src/gui/bubbles/reasoning.py b/engine/src/gui/bubbles/reasoning.py
index 4624924..71691ef 100644
--- a/engine/src/gui/bubbles/reasoning.py
+++ b/engine/src/gui/bubbles/reasoning.py
@@ -4,7 +4,7 @@
from typing import List, Optional, Tuple
-from PySide6.QtCore import Qt, Signal, QTimer, QObject, QSize
+from PySide6.QtCore import Qt, Signal, QTimer, QObject, QSize, Property, QPropertyAnimation, QEasingCurve
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame,
QPushButton, QSizePolicy,
@@ -46,6 +46,7 @@ def __init__(self, parent=None):
self._output_tokens = 0
self._reasoning_tokens = 0
self._displayed_tokens = 0 # 当前显示的 token 数字(用于动画插值)
+ self._token_anim: Optional[QPropertyAnimation] = None
self._token_bridge: Optional[QObject] = None
self.setStyleSheet("background:transparent;")
@@ -443,7 +444,6 @@ def _tool_mode_for_line(status_type: str, text: str) -> str:
return "gear"
def _rerender_tool_status(self):
- from src.gui.style import STATUS_COLORS
color_map = STATUS_COLORS
prefix_map = {
"coding": "[CODE] ",
diff --git a/engine/src/gui/bubbles/tool_bubbles.py b/engine/src/gui/bubbles/tool_bubbles.py
index 32b5415..6783cb0 100644
--- a/engine/src/gui/bubbles/tool_bubbles.py
+++ b/engine/src/gui/bubbles/tool_bubbles.py
@@ -8,7 +8,7 @@
Qt, Signal, QSize,
)
from PySide6.QtWidgets import (
- QVBoxLayout, QHBoxLayout, QLabel, QFrame, QSizePolicy,
+ QVBoxLayout, QHBoxLayout, QLabel, QFrame, QSizePolicy, QPushButton, QWidget,
)
from src.gui.style import RADII, COLORS, FONT_STACK, FONT_CODE, FONT_WEIGHTS, SIZES, tr
diff --git a/engine/src/gui/panels/left_panel.py b/engine/src/gui/panels/left_panel.py
index aade536..6265996 100644
--- a/engine/src/gui/panels/left_panel.py
+++ b/engine/src/gui/panels/left_panel.py
@@ -6,7 +6,7 @@
from PySide6.QtCore import (
Qt, QSize, Signal, QPropertyAnimation, QEasingCurve, QEvent,
- QPoint, Property,
+ QPoint, Property, QTimer,
)
from PySide6.QtGui import (
QPixmap, QIcon, QColor, QPainter, QPen, QBrush, QTextCursor,
diff --git a/engine/src/gui/slash_popup.py b/engine/src/gui/slash_popup.py
index c90dc97..2f8f015 100644
--- a/engine/src/gui/slash_popup.py
+++ b/engine/src/gui/slash_popup.py
@@ -9,6 +9,7 @@
from __future__ import annotations
from typing import List, Optional, Tuple
+from src.gui.animations._helpers import _stop_existing_anim
from PySide6.QtCore import (
Property,
diff --git a/engine/src/gui/style/_i18n.py b/engine/src/gui/style/_i18n.py
index 2754001..ab2bcd8 100644
--- a/engine/src/gui/style/_i18n.py
+++ b/engine/src/gui/style/_i18n.py
@@ -167,7 +167,6 @@ class I18N:
"add_provider": "添加供应商",
"remove_provider": "移除",
"nickname_label": "昵称",
- "nickname_placeholder": "你的昵称(选填)",
"custom_rules_label": "自定义规则",
"custom_rules_placeholder": "添加你希望AI遵守的自定义规则...",
"user_agreement": "查看用户协议",
@@ -520,7 +519,6 @@ class I18N:
"add_provider": "Add Provider",
"remove_provider": "Remove",
"nickname_label": "Nickname",
- "nickname_placeholder": "Your nickname (optional)",
"custom_rules_label": "Custom Rules",
"custom_rules_placeholder": "Add custom rules for AI to follow...",
"user_agreement": "User Agreement",
diff --git a/engine/src/mcp/client.py b/engine/src/mcp/client.py
index 5189c71..1f147c2 100644
--- a/engine/src/mcp/client.py
+++ b/engine/src/mcp/client.py
@@ -116,6 +116,9 @@ def __init__(
self._available_tools: List[Dict[str, Any]] = []
self._lock: Any = None # asyncio.Lock,在持久化 loop 中创建
self._running = False
+ self._connection_task = None
+ self._start_future = None
+ self._stop_requested = None
# 持久化 event loop(后台线程)
self._loop: Optional[asyncio.AbstractEventLoop] = None
@@ -292,114 +295,69 @@ def _run_on_loop(self, coro, timeout: Optional[float] = None) -> Any:
# --------------------- 异步实现 ---------------------
async def start_async(self) -> Dict[str, Any]:
- """异步启动 MCP 服务器,完成 MCP handshake (initialize),返回服务器信息。
-
- 返回:
- {"server_name": str, "version": str, "capabilities": {...}, "ok": True}
- 失败时返回 {"ok": False, "error": str}
- """
if self._running:
- return {"ok": True, **self._server_info}
-
+ return dict(self._server_info)
if not _MCP_AVAILABLE:
- return {
- "ok": False,
- "error": "MCP Python SDK 未安装 (pip install mcp>=1.27),无法启动 MCP 服务器",
- "server_name": self.server_name,
- }
-
- # 在持久化 loop 中创建锁(确保绑定到当前运行中的 loop)
- with self._lock_lock:
- if self._lock is None:
- self._lock = asyncio.Lock()
-
+ return {"ok": False, "error": "MCP Python SDK 未安装 (pip install mcp)",
+ "server_name": self.server_name}
+ if self._connection_task is None or self._connection_task.done():
+ self._lock = asyncio.Lock()
+ self._stop_requested = asyncio.Event()
+ self._start_future = asyncio.get_running_loop().create_future()
+ self._connection_task = asyncio.create_task(self._connection_owner(self._start_future))
try:
- server_params = StdioServerParameters(
- command=self.command,
- args=list(self.args),
- env=dict(self.env) if self.env else None,
- cwd=self.working_dir,
- )
- # 直接手动 enter,以获取可跨多次调用存活的 read/write stream
- cm = stdio_client(server_params)
- read_stream, write_stream = await cm.__aenter__()
- self._cm_exit_stack = cm
-
- session = ClientSession(read_stream, write_stream)
- init_result = await session.initialize()
-
- self._session = session
- self._running = True
-
- # 解析 server_info
- try:
- info_dict = (
- init_result.serverInfo.model_dump()
- if hasattr(init_result, "serverInfo") and init_result.serverInfo
- else {}
- )
- except Exception:
- info_dict = {}
-
- try:
- caps = (
- init_result.capabilities.model_dump()
- if hasattr(init_result, "capabilities") and init_result.capabilities
- else {}
- )
- except Exception:
- caps = {}
-
- self._server_info = {
- "ok": True,
- "server_name": info_dict.get("name", self.server_name),
- "version": info_dict.get("version", ""),
- "capabilities": caps,
- }
-
- # 初始化后立即拿一次工具列表
- try:
- self._available_tools = await self._list_tools_async_locked()
- except Exception as exc: # pragma: no cover - 仅记录
- logger.warning("list_tools 初始化失败: %s", exc)
+ return await asyncio.shield(self._start_future)
+ except asyncio.CancelledError:
+ await self.stop_async()
+ raise
- return dict(self._server_info)
-
- except Exception as exc:
- logger.exception("MCP 服务器 %s 启动失败", self.server_name)
- # 尝试清理
- try:
- if self._cm_exit_stack is not None:
- await self._cm_exit_stack.__aexit__(type(exc), exc, None)
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
- self._cm_exit_stack = None
- self._session = None
- self._running = False
- return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
-
- async def stop_async(self) -> None:
- """优雅关闭会话与子进程。"""
- if not self._running:
- return
- self._running = False
+ async def _connection_owner(self, ready):
+ """Enter and exit both SDK context managers in the same owning task."""
try:
- # 通知服务端结束会话 (可选)
- if self._session is not None and hasattr(self._session, "close"):
- try:
- await asyncio.wait_for(self._session.close(), timeout=1.0)
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
+ params = StdioServerParameters(command=self.command, args=list(self.args),
+ env=dict(self.env) if self.env else None,
+ cwd=self.working_dir)
+ async with stdio_client(params) as (read_stream, write_stream):
+ async with ClientSession(read_stream, write_stream) as session:
+ self._session = session
+ initialized = await session.initialize()
+ self._server_info = {"ok": True,
+ "server_name": initialized.serverInfo.name,
+ "version": initialized.serverInfo.version,
+ "capabilities": initialized.capabilities.model_dump()}
+ self._available_tools = await self._list_tools_async_locked()
+ self._running = True
+ if not ready.done():
+ ready.set_result(dict(self._server_info))
+ await self._stop_requested.wait()
+ except asyncio.CancelledError:
+ if not ready.done():
+ ready.set_result({"ok": False, "error": "MCP 启动已取消"})
+ raise
+ except Exception as exc:
+ logger.warning("MCP 服务器 %s 连接失败: %s", self.server_name, exc)
+ if not ready.done():
+ ready.set_result({"ok": False, "error": f"{type(exc).__name__}: {exc}"})
finally:
- if self._cm_exit_stack is not None:
- try:
- await self._cm_exit_stack.__aexit__(None, None, None)
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
- self._cm_exit_stack = None
+ self._running = False
self._session = None
self._available_tools = []
+ async def stop_async(self) -> None:
+ if self._stop_requested is not None:
+ self._stop_requested.set()
+ task = self._connection_task
+ if task is not None and task is not asyncio.current_task():
+ try:
+ await asyncio.wait_for(asyncio.shield(task), timeout=3)
+ except asyncio.TimeoutError:
+ task.cancel()
+ await asyncio.gather(task, return_exceptions=True)
+ except asyncio.CancelledError:
+ task.cancel()
+ await asyncio.gather(task, return_exceptions=True)
+ self._connection_task = self._start_future = self._stop_requested = None
+
async def _list_tools_async_locked(self) -> List[Dict[str, Any]]:
"""已持有锁的内部 list_tools 实现。"""
if self._session is None:
@@ -541,7 +499,7 @@ def start(self, timeout: float = 30.0) -> Dict[str, Any]:
def stop(self) -> None:
"""同步停止。"""
with self._thread_lock:
- if not self._running:
+ if self._loop is None:
return
try:
self._run_on_loop(self.stop_async(), timeout=5.0)
diff --git a/engine/src/sandbox/history.py b/engine/src/sandbox/history.py
index 82f9a72..e9f866e 100644
--- a/engine/src/sandbox/history.py
+++ b/engine/src/sandbox/history.py
@@ -16,50 +16,24 @@
logger = logging.getLogger(__name__)
import json
+import copy
import os
-import shutil
-import tempfile
import threading
import time
from pathlib import Path
from typing import List, Dict, Optional
+from src.core.atomic_files import atomic_write_bytes
def _atomic_write_history(path: Path, payload: Dict) -> bool:
- """原子化写入 history JSON:先写临时文件 -> fsync -> os.replace。
-
- 避免进程在写入中途被杀导致 session 文件残留半拉 JSON(不可恢复)。
- """
+ """Keep the previous complete session when serialization or replacement fails."""
target = Path(path)
try:
target.parent.mkdir(parents=True, exist_ok=True)
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
- try:
- fd, tmp_name = tempfile.mkstemp(
- prefix=".history_", suffix=".tmp", dir=str(target.parent)
- )
- try:
- with os.fdopen(fd, "w", encoding="utf-8") as f:
- json.dump(payload, f, ensure_ascii=False, indent=2)
- f.flush()
- try:
- os.fsync(f.fileno())
- except OSError as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
- try:
- os.replace(tmp_name, str(target))
- except OSError:
- shutil.move(tmp_name, str(target))
- return True
- except Exception:
- try:
- os.unlink(tmp_name)
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
- raise
- except Exception as e:
- print(f"[History] 原子写入失败 ({target}): {e}")
+ encoded = json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False).encode("utf-8")
+ return atomic_write_bytes(target, encoded)
+ except (OSError, ValueError, TypeError) as exc:
+ logger.warning("历史写入失败: %s", exc)
return False
@@ -78,7 +52,7 @@ def __init__(self, history_dir: Path, skip_load: bool = False):
logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
# 完整消息列表, 每项: {"role": ..., "content": ..., "reasoning_content": ..., ...}
self.messages: List[Dict] = []
- self.current_session_file = self.dir / f"session_{time.strftime('%Y%m%d_%H%M%S')}.json"
+ self.current_session_file = self.dir / f"session_{time.strftime('%Y%m%d_%H%M%S')}_{time.time_ns()}.json"
self.tool_call_counter = 0 # 本轮会话中的工具调用次数 (用于新代理判定)
self._dirty = False # 标记是否有未保存的修改
self._dirty_version = 0 # P0-2: 单调递增的脏版本号,用于检测快照后的并发修改
@@ -150,7 +124,7 @@ def _load_latest(self):
print(f"[History] 警告: {len(files)} 个 session 文件无法加载,创建新会话")
self.messages = []
self.tool_call_counter = 0
- self.current_session_file = self.dir / f"session_{time.strftime('%Y%m%d_%H%M%S')}.json"
+ self.current_session_file = self.dir / f"session_{time.strftime('%Y%m%d_%H%M%S')}_{time.time_ns()}.json"
def replace_messages(self, messages: list):
"""直接替换内存中的消息列表,不调用 _load_latest;并立刻重算 tool_call_counter。
@@ -159,7 +133,7 @@ def replace_messages(self, messages: list):
线程安全:持锁写入。
"""
if isinstance(messages, list):
- cleaned = [m for m in messages if isinstance(m, dict)]
+ cleaned = copy.deepcopy([m for m in messages if isinstance(m, dict)])
else:
cleaned = []
tool_counter = 0
@@ -221,28 +195,8 @@ def save(self):
payload = {"messages": snapshot, "saved_at": time.time()}
ok = _atomic_write_history(target_file, payload)
if not ok:
- # 回退也使用原子写入(临时文件+rename),避免进程崩溃导致会话文件损坏
- import tempfile as _tmp
- try:
- target_file.parent.mkdir(parents=True, exist_ok=True)
- fd, tmp_path = _tmp.mkstemp(
- dir=str(target_file.parent),
- prefix=".history_",
- suffix=".tmp",
- )
- try:
- with os.fdopen(fd, "w", encoding="utf-8") as f:
- json.dump(payload, f, ensure_ascii=False, indent=2)
- os.replace(tmp_path, str(target_file))
- except Exception:
- try:
- os.unlink(tmp_path)
- except OSError:
- pass
- raise
- except Exception as e2:
- print(f"[History] fallback 写入也失败: {e2}")
- return # 写入失败,不更新 _dirty,下次重试
+ logger.warning("历史未保存,将在下次保存时重试: %s", target_file)
+ return # Keep the dirty state and the previous complete file.
# P0-2 修复: 在锁内更新时间戳和脏标志,使用版本号精确检测并发修改
self._last_save_time = time.time()
@@ -260,9 +214,11 @@ def _trim_messages(self) -> None:
if len(self.messages) > self._MAX_MESSAGES:
self.messages = self.messages[-self._MAX_MESSAGES:]
- def add_user_message(self, content: str) -> Dict:
+ def add_user_message(self, content: str, display_content: Optional[str] = None) -> Dict:
"""保存完整用户消息(线程安全)。"""
msg = {"role": "user", "content": str(content or ""), "timestamp": time.time()}
+ if display_content is not None:
+ msg["display_content"] = str(display_content)
with self._lock:
if not isinstance(self.messages, list):
self.messages = []
@@ -274,7 +230,7 @@ def add_user_message(self, content: str) -> Dict:
self.save()
except Exception as e:
print(f"[History] add_user_message 落盘失败: {e}")
- return msg
+ return copy.deepcopy(msg)
def add_assistant_message(self, content: str, reasoning_content: str = "", tool_calls: Optional[List[Dict]] = None) -> Dict:
"""保存完整助手回复(线程安全)。"""
@@ -287,7 +243,7 @@ def add_assistant_message(self, content: str, reasoning_content: str = "", tool_
cleaned_calls: List[Dict] = []
if isinstance(tool_calls, list) and tool_calls:
# 过滤非 dict 项,避免下游遍历炸
- cleaned_calls = [c for c in tool_calls if isinstance(c, dict)]
+ cleaned_calls = copy.deepcopy([c for c in tool_calls if isinstance(c, dict)])
msg["tool_calls"] = cleaned_calls
with self._lock:
if not isinstance(self.messages, list):
@@ -302,9 +258,9 @@ def add_assistant_message(self, content: str, reasoning_content: str = "", tool_
self.save()
except Exception as e:
print(f"[History] add_assistant_message 落盘失败: {e}")
- return msg
+ return copy.deepcopy(msg)
- def add_tool_result(self, tool_name: str, result_text: str, tool_call_id: str = "") -> Dict:
+ def add_tool_result(self, tool_name: str, result_text: str, tool_call_id: str = "", ok: Optional[bool] = None) -> Dict:
"""保存完整工具调用结果(线程安全)。"""
msg = {
"role": "tool",
@@ -312,6 +268,8 @@ def add_tool_result(self, tool_name: str, result_text: str, tool_call_id: str =
"content": str(result_text or ""),
"timestamp": time.time(),
}
+ if ok is not None:
+ msg["ok"] = bool(ok)
if tool_call_id:
msg["tool_call_id"] = tool_call_id
with self._lock:
@@ -325,13 +283,18 @@ def add_tool_result(self, tool_name: str, result_text: str, tool_call_id: str =
self.save()
except Exception as e:
print(f"[History] add_tool_result 落盘失败: {e}")
- return msg
+ return copy.deepcopy(msg)
+
+ def snapshot(self) -> List[Dict]:
+ """Return a detached snapshot for UI/API consumers."""
+ with self._lock:
+ return copy.deepcopy(self.messages)
def clear(self):
"""清空当前会话, 新建一个 session 文件(线程安全)。
P1-3 修复: 在锁内设置 _dirty=True 并递增版本号,确保空列表被保存。
"""
- new_path = self.dir / f"session_{time.strftime('%Y%m%d_%H%M%S')}.json"
+ new_path = self.dir / f"session_{time.strftime('%Y%m%d_%H%M%S')}_{time.time_ns()}.json"
with self._lock:
self.messages = []
self.tool_call_counter = 0
@@ -355,26 +318,23 @@ def to_api_messages(self, system_prompt: str, language: str = "zh") -> List[Dict
- system prompt
- user: 完整原文
- assistant: 完整 content (reasoning 不放入 content 以确保缓存命中)
- - tool: 完整结果, 以 role=user 形式包装 (因为 deepseek 原生不支持 tool role)
+ - tool: 原生调用使用 role=tool,旧文本调用使用 role=user 包装
参数:
language: 界面语言,用于工具结果包装前缀(B-15)。
"""
# 对 messages 做一次锁内快照,避免遍历期间被并发修改
- with self._lock:
- if not isinstance(self.messages, list):
- self.messages = []
- snapshot = list(self.messages)
+ snapshot = self.snapshot()
- messages = [{"role": "system", "content": system_prompt, "id": "msg_sys"}]
- for i, m in enumerate(snapshot, 1):
+ messages = [{"role": "system", "content": system_prompt}]
+ for m in snapshot:
role = m.get("role", "user")
content = m.get("content", "")
if role == "tool":
tc_id = m.get("tool_call_id", "")
if tc_id:
# 原生 function calling: 使用标准 role=tool + tool_call_id
- messages.append({"role": "tool", "tool_call_id": tc_id, "content": content, "id": f"msg_{i}"})
+ messages.append({"role": "tool", "tool_call_id": tc_id, "content": content})
else:
# 文本提取或子代理: 回退到 role=user 包装
name = m.get("name", "tool")
@@ -382,19 +342,30 @@ def to_api_messages(self, system_prompt: str, language: str = "zh") -> List[Dict
wrapper = f"[Tool Result - {name}]\n{content}"
else:
wrapper = f"[工具调用结果 - {name}]\n{content}"
- messages.append({"role": "user", "content": wrapper, "id": f"msg_{i}"})
+ messages.append({"role": "user", "content": wrapper})
elif role == "assistant":
- msg = {"role": "assistant", "content": content, "id": f"msg_{i}"}
+ msg = {"role": "assistant", "content": content}
# DeepSeek V4 要求工具调用后的轮次必须回传 reasoning_content
rc = m.get("reasoning_content", "")
- tc = m.get("tool_calls")
- if rc:
+ tc = []
+ for call in m.get("tool_calls") or []:
+ if not isinstance(call, dict):
+ continue
+ fn = call.get("function")
+ if not isinstance(fn, dict) or not fn.get("name") or not call.get("id"):
+ continue
+ arguments = fn.get("arguments", "{}")
+ if not isinstance(arguments, str):
+ arguments = json.dumps(arguments, ensure_ascii=False)
+ tc.append({"id": call["id"], "type": "function",
+ "function": {"name": fn["name"], "arguments": arguments}})
+ if rc or tc:
msg["reasoning_content"] = rc
if tc:
msg["tool_calls"] = tc
messages.append(msg)
elif role == "user":
- messages.append({"role": "user", "content": content, "id": f"msg_{i}"})
+ messages.append({"role": "user", "content": content})
return messages
# ---------- 上下文摘要 (仅给新代理启动用) ----------
@@ -421,4 +392,4 @@ def compress_context(self) -> str:
# COT 砍掉,只保留正文第一行
first_line = content.split("\n")[0][:120]
lines.append(f"[助手] {first_line}")
- return "\n".join(lines)
\ No newline at end of file
+ return "\n".join(lines)
diff --git a/engine/src/sandbox/project_manager.py b/engine/src/sandbox/project_manager.py
index 7e1e569..ec3b784 100644
--- a/engine/src/sandbox/project_manager.py
+++ b/engine/src/sandbox/project_manager.py
@@ -21,6 +21,7 @@
import threading
import time
import unicodedata
+import weakref
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
@@ -36,69 +37,15 @@
def _atomic_write_text(path: Path, text: str, encoding: str = "utf-8") -> bool:
- """原子写入文本文件:在已验证的父目录 fd 内创建临时文件再 os.replace,
- 避免写入一半被读,并防止目标/父目录被替换为符号链接导致的沙箱逃逸。
- """
- target = Path(path)
- parent = target.parent
- try:
- # 打开父目录并禁止跟随符号链接;若父目录是 symlink,直接失败
- _O_DIRECTORY = getattr(os, "O_DIRECTORY", 0)
- _O_NOFOLLOW = getattr(os, "O_NOFOLLOW", 0)
- dir_fd = os.open(str(parent), os.O_RDONLY | _O_DIRECTORY | _O_NOFOLLOW)
- except OSError:
- return False
- tmp_name: Optional[str] = None
- tmp_fd: Optional[int] = None
- try:
- # 在 dir_fd 对应的目录中创建唯一临时文件(O_EXCL | O_NOFOLLOW)
- for _ in range(10):
- tmp_name = f".tmp_{os.getpid()}_{os.urandom(8).hex()}"
- try:
- tmp_fd = os.open(tmp_name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | _O_NOFOLLOW,
- 0o600, dir_fd=dir_fd)
- break
- except FileExistsError as _exc:
- logger.debug("跳过异常项: %s", _exc, exc_info=True)
- continue
- if tmp_fd is None:
- return False
- try:
- with os.fdopen(tmp_fd, "w", encoding=encoding, closefd=True) as f:
- f.write(text)
- # 在已验证的目录 fd 内原子替换目标文件
- os.replace(tmp_name, target.name, src_dir_fd=dir_fd, dst_dir_fd=dir_fd)
- # M-003: 创建/替换后限制文件权限为 0o600
- try:
- os.chmod(str(target), 0o600, follow_symlinks=False)
- except OSError as e:
- logger.warning("[_atomic_write_text] 设置权限 0o600 失败: %s (%s)", target, e)
- return True
- except Exception:
- # fdopen 成功时 closefd=True 会自动关闭 fd;fdopen 失败时需手动关闭
- try:
- os.close(tmp_fd)
- except OSError:
- pass
- try:
- if tmp_name:
- os.unlink(tmp_name, dir_fd=dir_fd)
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
- return False
- finally:
- try:
- os.close(dir_fd)
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
+ from src.core.atomic_files import atomic_write_bytes
+ return atomic_write_bytes(path, text.encode(encoding))
def _atomic_write_json(path: Path, data) -> bool:
"""原子化写入 JSON 文件。对数据合法性做最小校验,避免写一半损坏。"""
try:
# 只允许 JSON 可序列化的基本类型;拒绝自定义对象
- text = json.dumps(data, ensure_ascii=False, indent=2,
- default=lambda o: getattr(o, "__dict__", str(o)))
+ text = json.dumps(data, ensure_ascii=False, indent=2, allow_nan=False)
except (TypeError, ValueError) as e:
logger.info("[ProjectSandbox] JSON 序列化失败")
logger.debug("[ProjectSandbox] JSON 序列化失败: %s", e, exc_info=True)
@@ -231,10 +178,20 @@ class ProjectSandbox:
# 项目根目录下回收站文件夹名称(英文,按要求固定)
RECYCLE_BIN_NAME = "RecycleBin"
+ _locks = weakref.WeakValueDictionary()
+ _locks_guard = threading.Lock()
+
def __init__(self, name: str, root: Path):
+ raw_root = Path(root).absolute()
+ if raw_root.is_symlink():
+ raise ValueError("Project root cannot be a symbolic link")
+ for child in ("work", "preview", "history", self.RECYCLE_BIN_NAME, "DS.md", "meta.json"):
+ if (raw_root / child).is_symlink():
+ raise ValueError("Project data cannot be a symbolic link")
self.name = unicodedata.normalize("NFC", str(name))
self.root = Path(root).resolve()
- self._lock = threading.RLock() # 保护对 meta/ds_md 等共享资源的读写
+ with self._locks_guard:
+ self._lock = self._locks.setdefault(str(self.root), threading.RLock())
self.work_dir = self.root / "work"
self.preview_dir = self.root / "preview"
self.history_dir = self.root / "history"
@@ -247,7 +204,8 @@ def __init__(self, name: str, root: Path):
d.mkdir(parents=True, exist_ok=True, mode=0o700)
if not self.ds_md.exists():
- self.ds_md.write_text(_build_default_ds_md(name), encoding="utf-8")
+ if not _atomic_write_text(self.ds_md, _build_default_ds_md(name)):
+ raise OSError("Failed to initialize DS.md")
try:
os.chmod(str(self.ds_md), 0o600, follow_symlinks=False)
except OSError as e:
@@ -316,31 +274,10 @@ def _migrate_old_format(self, old_content: str):
logger.warning("[ProjectSandbox] 设置 DS.md 权限 0o600 失败: %s (%s)", self.ds_md, e)
# ---------- meta ----------
- def _save_meta(self, meta: Dict):
- """原子化保存项目元信息(线程安全:持锁写入)。"""
+ def _save_meta(self, meta: Dict) -> bool:
+ """Persist metadata atomically and report the actual write result."""
with self._lock:
- try:
- if not isinstance(meta, dict):
- meta = {}
- ok = _atomic_write_json(self.meta_file, meta)
- if ok:
- try:
- os.chmod(str(self.meta_file), 0o600, follow_symlinks=False)
- except OSError as e:
- logger.warning("[ProjectSandbox] 设置 meta.json 权限 0o600 失败: %s (%s)", self.meta_file, e)
- else:
- try:
- with open(self.meta_file, "w", encoding="utf-8") as f:
- json.dump(meta, f, ensure_ascii=False, indent=2)
- try:
- os.chmod(str(self.meta_file), 0o600, follow_symlinks=False)
- except OSError as e:
- logger.warning("[ProjectSandbox] 设置 meta.json 权限 0o600 失败: %s (%s)", self.meta_file, e)
- except Exception as _exc:
- logger.debug("忽略可恢复异常: %s", _exc, exc_info=True)
- except Exception as e:
- logger.info("[ProjectSandbox] _save_meta 失败")
- logger.debug("[ProjectSandbox] _save_meta 失败: %s", e, exc_info=True)
+ return isinstance(meta, dict) and _atomic_write_json(self.meta_file, meta)
def _load_meta(self) -> Dict:
"""以字节形式读取元信息(线程安全:持锁读取)。"""
@@ -401,8 +338,7 @@ def set_display_name(self, name: str, user_initiated: bool = False) -> bool:
if user_initiated:
meta["renamed_by_user"] = True
meta["last_access"] = time.time()
- self._save_meta(meta)
- return True
+ return self._save_meta(meta)
def is_pinned(self) -> bool:
meta = self._load_meta()
@@ -518,32 +454,8 @@ def _replace_import(match):
return re.sub(r'@import\s+["\']([^"\']+)["\']', _replace_import, text)
def write_ds_md(self, content: str) -> bool:
- """整体写入 DS.md(线程安全:持锁写入)。"""
with self._lock:
- try:
- text = str(content or "")
- ok = _atomic_write_text(self.ds_md, text)
- if ok:
- try:
- os.chmod(str(self.ds_md), 0o600, follow_symlinks=False)
- except OSError as e:
- logger.warning("[ProjectSandbox] 设置 DS.md 权限 0o600 失败: %s (%s)", self.ds_md, e)
- return True
- try:
- self.ds_md.write_text(text, encoding="utf-8")
- try:
- os.chmod(str(self.ds_md), 0o600, follow_symlinks=False)
- except OSError as e:
- logger.warning("[ProjectSandbox] 设置 DS.md 权限 0o600 失败: %s (%s)", self.ds_md, e)
- return True
- except Exception as e2:
- logger.info("[ProjectSandbox] DS.md fallback 写入失败")
- logger.debug("[ProjectSandbox] DS.md fallback 写入失败: %s", e2, exc_info=True)
- return False
- except Exception as e:
- logger.info("[ProjectSandbox] 写入 DS.md 失败")
- logger.debug("[ProjectSandbox] 写入 DS.md 失败: %s", e, exc_info=True)
- return False
+ return _atomic_write_text(self.ds_md, str(content or ""))
def read_section(self, section_key: str) -> str:
if section_key not in DS_SECTIONS:
@@ -559,53 +471,39 @@ def write_section(self, section_key: str, body: str) -> bool:
new_content = _replace_section(content, section_key, body)
return self.write_ds_md(new_content)
- def move_task_to_completed(self, task_text: str) -> bool:
- if not task_text or not str(task_text).strip():
+ def replace_tasks(self, tasks: List[Dict]) -> bool:
+ """Update pending and completed sections in one atomic write."""
+ if not isinstance(tasks, list) or len(tasks) > 1000:
return False
- sections = _parse_sections(self.read_ds_md())
- pending = sections.get("pending", "")
- completed = sections.get("completed", "")
-
- new_pending_lines = []
- found = False
- for line in pending.splitlines():
- m = re.match(r"^\s*[-*]\s*\[\s*\]\s*(.+)$", line)
- if m and task_text.strip() in m.group(1).strip():
- found = True
- continue
- new_pending_lines.append(line)
- if not found:
- for line in pending.splitlines():
- if task_text.strip() in line and "[ ]" in line:
- found = True
- new_pending_lines = [l for l in pending.splitlines() if l != line]
- break
- if not found:
- return False
-
- new_pending = "\n".join(new_pending_lines).strip() or "(暂无未完成任务)"
- default_empty = "(暂无已完成任务)"
- if completed.strip() == default_empty:
- new_completed = f"- [x] {task_text.strip()}"
- else:
- new_completed = completed.strip() + "\n" + f"- [x] {task_text.strip()}"
+ for item in tasks:
+ if (not isinstance(item, dict) or type(item.get("done")) is not bool
+ or not isinstance(item.get("text"), str) or not item["text"].strip()
+ or len(item["text"]) > 4096 or "\n" in item["text"] or "\r" in item["text"]):
+ return False
+ with self._lock:
+ content = self.read_ds_md()
+ for section, done in (("pending", False), ("completed", True)):
+ body = "\n".join(f"- [{'x' if done else ' '}] {t['text'].strip()}"
+ for t in tasks if t["done"] is done)
+ content = _replace_section(content, section, body)
+ return self.write_ds_md(content)
- self.write_section("pending", new_pending)
- if not self.write_section("completed", new_completed):
- # 回滚:将任务放回 pending
- self.write_section("pending", pending)
+ def move_task_to_completed(self, task_text: str) -> bool:
+ if not isinstance(task_text, str) or not task_text.strip():
+ return False
+ with self._lock:
+ tasks = self.get_tasks()
+ for item in tasks:
+ if not item["done"] and item["text"] == task_text.strip():
+ item["done"] = True
+ return self.replace_tasks(tasks)
return False
- return True
def add_pending_task(self, task_text: str) -> bool:
- if not task_text or not str(task_text).strip():
+ if not isinstance(task_text, str) or not task_text.strip():
return False
- body = self.read_section("pending")
- if body.startswith("(") and "暂" in body:
- new_body = f"- [ ] {task_text.strip()}"
- else:
- new_body = body.strip() + "\n" + f"- [ ] {task_text.strip()}"
- return self.write_section("pending", new_body)
+ with self._lock:
+ return self.replace_tasks(self.get_tasks() + [{"text": task_text.strip(), "done": False}])
def get_tasks(self) -> List[Dict]:
sections = _parse_sections(self.read_ds_md())
@@ -652,6 +550,8 @@ def _safe_path(self, rel_path: str, is_work: bool = True) -> Optional[Path]:
return None
base = self.work_dir if is_work else self.preview_dir
+ if base.is_symlink():
+ return None
try:
base.mkdir(parents=True, exist_ok=True, mode=0o700)
except Exception:
@@ -668,8 +568,8 @@ def _safe_path(self, rel_path: str, is_work: bool = True) -> Optional[Path]:
return None
# 显式拒绝 . / .. 作为独立段(即使 resolve 会归一化,也提前拒绝可疑输入)
- rel_obj = Path(rel_str)
- if "." in rel_obj.parts or ".." in rel_obj.parts:
+ rel_str = rel_str.replace("\\", "/")
+ if any(part in {".", ".."} for part in rel_str.split("/")):
return None
try:
@@ -831,7 +731,7 @@ def write_file(self, rel_path: str, content: str) -> bool:
return False
if not isinstance(content, str):
content = str(content or "")
- if len(content) > self._MAX_WRITE_BYTES:
+ if len(content.encode("utf-8")) > self._MAX_WRITE_BYTES:
logger.info("[ProjectSandbox] 写入被拒绝 (超出单文件上限): %s", rel_path)
return False
target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
@@ -1145,7 +1045,7 @@ def list_project_infos(self) -> List[Dict]:
try:
for p in sorted(self.projects_root.iterdir()):
try:
- if not p.is_dir() or p.name.startswith("."):
+ if p.is_symlink() or not p.is_dir() or p.name.startswith("."):
continue
sb = ProjectSandbox(p.name, p)
meta = sb.get_meta()
@@ -1205,7 +1105,7 @@ def _safe_project_name(name: str) -> Optional[str]:
return None
name = unicodedata.normalize("NFC", name.strip())
# H-007:拒绝空名、纯点号、以点开头/结尾
- if not name or name in (".", "..") or name.startswith(".") or name.endswith("."):
+ if not name or len(name.encode("utf-8")) > 200 or name in (".", "..") or name.startswith(".") or name.endswith("."):
return None
if name.startswith("-"):
return None
@@ -1228,9 +1128,12 @@ def get(self, name: str) -> Optional[ProjectSandbox]:
if not self._safe_project_name(name):
return None
root = self.projects_root / name
- if not root.exists():
+ if root.is_symlink() or not root.is_dir():
+ return None
+ try:
+ return ProjectSandbox(name, root)
+ except (OSError, ValueError):
return None
- return ProjectSandbox(name, root)
def create(self, name: str, display_name: Optional[str] = None) -> Optional[ProjectSandbox]:
if not self._safe_project_name(name):
@@ -1238,6 +1141,8 @@ def create(self, name: str, display_name: Optional[str] = None) -> Optional[Proj
return None
root = self.projects_root / name
with self._lock:
+ if root.is_symlink():
+ return None
try:
root.mkdir(parents=True, exist_ok=True, mode=0o700)
except FileExistsError:
@@ -1249,7 +1154,10 @@ def create(self, name: str, display_name: Optional[str] = None) -> Optional[Proj
except Exception as _exc:
logger.warning("创建项目失败:%s", _exc, exc_info=True)
return None
- sb = ProjectSandbox(name, root)
+ try:
+ sb = ProjectSandbox(name, root)
+ except (OSError, ValueError):
+ return None
if display_name:
sb.set_display_name(display_name, user_initiated=False)
# M-018: 审计日志(项目创建)
@@ -1299,6 +1207,8 @@ def rename(self, old: str, new: str) -> bool:
with self._lock:
# H-003: 删除 dst.exists() 前置检查,直接使用原子 rename。
# FileExistsError 表示目标在检查窗口内被创建,按竞态处理。
+ if src.is_symlink() or dst.exists() or dst.is_symlink():
+ return False
try:
src.rename(dst)
return True
diff --git a/engine/src/tools/command_tools.py b/engine/src/tools/command_tools.py
index 4cf0e78..6049e6c 100644
--- a/engine/src/tools/command_tools.py
+++ b/engine/src/tools/command_tools.py
@@ -69,7 +69,7 @@ def _safe_terminate(proc: Optional[subprocess.Popen], force_kill: bool = False)
def _send(sig: int) -> None:
try:
- if pgid is not None and pgid > 0:
+ if pgid == proc.pid and pgid != os.getpgrp():
os.killpg(pgid, sig)
else:
os.kill(proc.pid, sig)
@@ -1830,7 +1830,7 @@ def _check_node(node, depth=0):
return False, f"[calculator] 计算失败: {e}"
# -------- 持久终端会话 --------
- def _get_terminal(self):
+ def _get_terminal(self, create=False):
"""获取或创建持久终端(懒加载,带锁防止并发创建多个进程)"""
with self._terminal_lock:
term = getattr(self._exec, "_persistent_terminal", None)
@@ -1843,7 +1843,7 @@ def _get_terminal(self):
except Exception:
pass
self._exec._persistent_terminal = None
- if self._exec.sandbox is None:
+ if not create or self._exec.sandbox is None:
return None
from src.tools.terminal_manager import TerminalManager
cwd = str(self._exec.sandbox.work_dir)
@@ -1860,7 +1860,7 @@ def _get_terminal(self):
def shell_start(self, call) -> Tuple[bool, str]:
"""启动或重启持久shell会话"""
- term = self._get_terminal()
+ term = self._get_terminal(create=True)
if term is None:
return False, "[shell_start] 无法启动shell(请检查bash/sh是否可用)"
return True, f"[shell_start] 持久shell已启动 (cwd={term.cwd})"
@@ -1947,6 +1947,9 @@ def shell_exec(self, call) -> Tuple[bool, str]:
output_lines: List[str] = []
exit_code = -1
while time.time() < deadline:
+ if self._exec.on_cancel_check and self._exec.on_cancel_check():
+ term.terminate()
+ return False, "[shell_exec] 已取消,终端已停止"
time.sleep(0.1)
out = term.read_output(clear=True)
if out:
@@ -1966,8 +1969,11 @@ def shell_exec(self, call) -> Tuple[bool, str]:
if len(result) > 8000:
result = result[:8000] + "\n... (输出已截断)"
+ if exit_code < 0:
+ term.terminate()
+ return False, f"[shell_exec] 超时或终端已退出,终端已停止\n{result}"
status = "✓" if exit_code == 0 else f"✗ (exit={exit_code})"
- return True, f"[shell_exec] {status}\n{result}"
+ return exit_code == 0, f"[shell_exec] {status}\n{result}"
def shell_send(self, call) -> Tuple[bool, str]:
"""向持久shell发送命令(不等待完成)"""
@@ -1978,7 +1984,7 @@ def shell_send(self, call) -> Tuple[bool, str]:
if self._exec._subagent_restrictions.get("forbid_commands", False):
return False, "[shell_send] 子代理不允许执行 shell 命令"
# reviewer模式禁止
- from ..config import Config
+ from src.core.config import Config
if Config().permission_mode == "reviewer":
return False, "[shell_send] reviewer模式下禁止执行shell命令"
# 危险模式预扫描
diff --git a/engine/src/tools/terminal_manager.py b/engine/src/tools/terminal_manager.py
index e17e229..4a41360 100644
--- a/engine/src/tools/terminal_manager.py
+++ b/engine/src/tools/terminal_manager.py
@@ -192,7 +192,7 @@ def _read_loop(self):
try:
while not self._closed:
try:
- chunk = stdout.read(1024)
+ chunk = getattr(stdout, "read1", stdout.read)(1024)
except Exception:
break
if not chunk:
diff --git a/engine/src/tools/text_tool_parser.py b/engine/src/tools/text_tool_parser.py
new file mode 100644
index 0000000..fc43d52
--- /dev/null
+++ b/engine/src/tools/text_tool_parser.py
@@ -0,0 +1,43 @@
+"""Parse complete JSON tool objects without guessing parameters from malformed output."""
+
+import json
+
+
+def extract_calls(text, names, max_chars=1024 * 1024, max_calls=50):
+ if not isinstance(text, str):
+ return []
+ text, calls, index = text[:max_chars], [], 0
+ while len(calls) < max_calls:
+ start = text.find("{", index)
+ if start < 0:
+ break
+ depth, quoted, escaped, end = 0, False, False, None
+ for position in range(start, len(text)):
+ char = text[position]
+ if quoted:
+ if escaped:
+ escaped = False
+ elif char == "\\":
+ escaped = True
+ elif char == '"':
+ quoted = False
+ elif char == '"':
+ quoted = True
+ elif char == "{":
+ depth += 1
+ elif char == "}":
+ depth -= 1
+ if depth == 0:
+ end = position + 1
+ break
+ if end is None:
+ break
+ raw = text[start:end]
+ try:
+ value = json.loads(raw, strict=False)
+ except (ValueError, RecursionError):
+ value = None
+ if isinstance(value, dict) and isinstance(value.get("tool"), str) and value["tool"] in names:
+ calls.append(value)
+ index = end
+ return calls
diff --git a/engine/src/tools/tool_executor.py b/engine/src/tools/tool_executor.py
index 70a7218..91841ff 100644
--- a/engine/src/tools/tool_executor.py
+++ b/engine/src/tools/tool_executor.py
@@ -115,6 +115,7 @@ class ToolExecutor:
VALID_SECTIONS = {"description", "pending", "completed", "custom"}
def __init__(self, sandbox, mcp_manager=None, sklii_manager=None, image_doc_manager=None, hook_manager=None, lsp_manager=None):
+ self._closed = False
self.sandbox = sandbox
self._mcp_manager = mcp_manager
self._sklii_manager = sklii_manager
@@ -197,209 +198,17 @@ def _is_subagent(self, value):
# ========== 解析 & 执行 ==========
# 单次提取最多处理多少字符,避免超长 reasoning 卡死循环
- _MAX_TOOL_SEARCH_LEN = 120_000
- _MAX_TOOL_CALLS = 50
-
def extract_tool_calls(self, text):
- """从模型输出(reasoning)中提取工具调用 JSON 数组。
+ from src.tools.text_tool_parser import extract_calls
+ return extract_calls(text, self.TOOL_NAMES)
- 对 None/空值/超长文本/无效 JSON 均有防御,避免下游收到异常数据。
- """
- calls = []
- if not isinstance(text, str):
- try:
- text = str(text or "")
- except Exception:
- return calls
- if not text:
- return calls
- # 超长文本:只扫描前段,避免推理输出数万字内容时陷入大循环
- if len(text) > self._MAX_TOOL_SEARCH_LEN:
- text = text[: self._MAX_TOOL_SEARCH_LEN]
-
- idx = 0
- safety_iter = 0
- max_iter = max(1000, text.count('"tool"') * 3 + 16)
- while safety_iter < max_iter:
- safety_iter += 1
- if len(calls) >= self._MAX_TOOL_CALLS:
- break
- pos = text.find('"tool"', idx)
- if pos == -1:
- break
- # C-2 修复: 使用字符串感知的方式向后查找开括号,避免定位到字符串内的 {
- open_brace = -1
- scan_i = pos - 1
- depth_back = 0
- in_str_back = False
- while scan_i >= 0:
- ch = text[scan_i]
- if ch == "\\" and in_str_back and scan_i > 0:
- # 转义字符:跳过下一个字符
- scan_i -= 2
- continue
- if ch == '"':
- in_str_back = not in_str_back
- elif not in_str_back:
- if ch == "}":
- depth_back += 1
- elif ch == "{":
- if depth_back == 0:
- open_brace = scan_i
- break
- depth_back -= 1
- scan_i -= 1
- if open_brace == -1:
- idx = pos + 6
- continue
- depth = 0
- close_brace = -1
- in_string = False
- i = open_brace
- # C-1 修复: 扩大扫描窗口到 64KB,支持大型工具调用(如 create_file 携带大 content)
- search_end = min(len(text), open_brace + 65536)
- while i < search_end:
- ch = text[i]
- if ch == "\\" and in_string and i + 1 < search_end:
- i += 2
- continue
- # JSON 标准仅使用双引号作为字符串界定符;单引号只是普通字符。
- if ch == '"':
- in_string = not in_string
- elif not in_string:
- if ch == "{":
- depth += 1
- elif ch == "}":
- depth -= 1
- if depth == 0:
- close_brace = i + 1
- break
- i += 1
- if close_brace == -1:
- # 工具调用 JSON 未闭合(可能超过 64KB 扫描窗口)
- logger.warning(
- "工具调用 JSON 未闭合,open_brace=%d, scan_window=%d, text_len=%d — 工具调用被跳过",
- open_brace, 65536, len(text)
- )
- idx = pos + 6
- continue
- try:
- raw = text[open_brace:close_brace]
- except Exception:
- idx = pos + 6
- continue
- data = self._try_parse_json(raw)
- if data and isinstance(data, dict) and data.get("tool") in self.TOOL_NAMES:
- calls.append(data)
- idx = close_brace
- continue
- try:
- fixed = self._fix_json_newlines(raw)
- except Exception:
- fixed = raw
- data = self._try_parse_json(fixed)
- if data and isinstance(data, dict) and data.get("tool") in self.TOOL_NAMES:
- calls.append(data)
- idx = close_brace
- continue
- # C-3 修复: 移除危险的 replace("'", '"') fallback。
- # 单引号替换会破坏含英文撇号的字符串值(如 "it's a bug" → "it"s a bug"),
- # 导致无效 JSON 或参数错误。直接走 manual_parse fallback。
- data = self._manual_parse(raw)
- if data and isinstance(data, dict) and data.get("tool") in self.TOOL_NAMES:
- calls.append(data)
- idx = close_brace
- return calls
-
- @staticmethod
- def _try_parse_json(s):
- try:
- return json.loads(s)
- except Exception:
- return None
-
- @staticmethod
- def _fix_json_newlines(s):
- result = []
- in_string = False
- i = 0
- while i < len(s):
- ch = s[i]
- if ch == "\\" and in_string and i + 1 < len(s):
- result.append(ch)
- result.append(s[i + 1])
- i += 2
- continue
- if ch == '"':
- if not in_string:
- in_string = True
- result.append(ch)
- else:
- in_string = False
- result.append(ch)
- elif in_string and ch == "\n":
- result.append("\\n")
- elif in_string and ch == "\r":
- result.append("\\r")
- elif in_string and ch == "\t":
- result.append("\\t")
- else:
- result.append(ch)
- i += 1
- return "".join(result)
-
- @staticmethod
- def _manual_parse(s):
- result = {}
- m = re.search(r'"tool"\s*:\s*["\']([\w_]+)["\']', s)
- if m:
- result["tool"] = m.group(1)
- m = re.search(r'"filename"\s*:\s*["\']([^"\']+)["\']', s)
- if m:
- result["filename"] = m.group(1)
- m = re.search(r'"section"\s*:\s*["\']([\w_]+)["\']', s)
- if m:
- result["section"] = m.group(1)
- m = re.search(r'"query"\s*:\s*["\']([^"\']+)["\']', s)
- if m:
- result["query"] = m.group(1)
- m = re.search(r'"url"\s*:\s*["\']([^"\']+)["\']', s)
- if m:
- result["url"] = m.group(1)
- m = re.search(r'"command"\s*:\s*["\']([^"\']+)["\']', s)
- if m:
- result["command"] = m.group(1)
- m = re.search(r'"task"\s*:\s*["\']([^"\']+)["\']', s)
- if m:
- result["task"] = m.group(1)
- m = re.search(r'"reason"\s*:\s*["\']([^"\']+)["\']', s)
- if m:
- result["reason"] = m.group(1)
- m = re.search(r'"content"\s*:\s*["\']', s)
- if m:
- start = m.end()
- quote_char = s[m.end() - 1] # 记录起始引号类型
- # 从起始位置查找匹配的闭合引号,限制搜索窗口防止跨对象匹配
- # 搜索窗口上限取剩余字符串长度和 128KB 的较小值
- search_end = min(len(s), start + 131072)
- # 手动查找未转义的闭合引号
- i = start
- while i < search_end:
- ch = s[i]
- if ch == "\\" and i + 1 < search_end:
- i += 2
- continue
- if ch == quote_char:
- end = i
- break
- i += 1
- else:
- end = -1
- if end > start:
- content = s[start:end]
- content = content.replace("\\n", "\n").replace("\\t", "\t").replace('\\"', '"')
- result["content"] = content
- return result if "tool" in result else None
+ def shutdown(self):
+ """Release resources owned by this executor; safe to call more than once."""
+ self._closed = True
+ terminal = getattr(self, "_persistent_terminal", None)
+ if terminal is not None:
+ terminal.terminate()
+ self._persistent_terminal = None
# 会修改项目状态的工具(子代理应禁止)
_WRITE_TOOLS = frozenset({
@@ -409,7 +218,7 @@ def _manual_parse(s):
"replace_in_files", "replace_symbol_in_files", "format_code",
"backup_workspace", "update_ds_section", "add_task", "complete_task",
# Git 状态变更
- "git_reset", "git_checkout", "git_stash", "git_branch_delete", "git_commit",
+ "git_reset", "git_checkout", "git_stash", "git_branch_delete", "git_commit", "git_add", "git_branch_create",
# 快照 / 撤销
"create_snapshot", "restore_snapshot", "undo_last",
# 测试 / 质量检查(可能产生副作用:写入缓存、触发钩子等)
@@ -463,11 +272,13 @@ def _manual_parse(s):
# ========== 统一分发 ==========
def execute(self, call):
+ if self._closed:
+ return False, "工具执行器已关闭"
# 防御: call 必须是非空 dict
if not isinstance(call, dict):
return False, f"[execute] 参数格式错误: 期望 dict,实际 {type(call).__name__}"
tool = call.get("tool", "")
- if not tool:
+ if not isinstance(tool, str) or not tool:
return False, "[execute] 缺少 tool 字段"
# C-005:调用链审计日志(记录 tool、子代理状态、调用链长度)
@@ -513,7 +324,7 @@ def execute(self, call):
# 权限模式前置拦截(reviewer 模式禁止写/删/状态变更类工具)
try:
- if Config().permission_mode == "reviewer" and tool in self._REVIEWER_BLOCKED_TOOLS:
+ if Config().permission_mode == "reviewer" and tool in (self._REVIEWER_BLOCKED_TOOLS | self._WRITE_TOOLS):
logger.info("[AUDIT] denied tool=%s reason=reviewer_mode", tool)
return False, f"[{tool}] reviewer 模式禁止执行该工具"
except Exception:
@@ -530,7 +341,7 @@ def execute(self, call):
blocked, reason, injection = self._hook_manager.trigger_blocking(
"PreToolUse",
tool_name=tool,
- tool_input=call.get("params", {}),
+ tool_input={k: v for k, v in call.items() if k != "tool"},
)
if blocked:
logger.info("[AUDIT] hook blocked tool=%s reason=%s", tool, reason)
@@ -548,7 +359,7 @@ def execute(self, call):
post_results = self._hook_manager.trigger(
"PostToolUse",
tool_name=tool,
- tool_input=call.get("params", {}),
+ tool_input={k: v for k, v in call.items() if k != "tool"},
tool_result=result_text,
tool_ok=result_ok,
)
@@ -568,298 +379,164 @@ def execute(self, call):
return result_ok, result_text
+ _TOOL_ROUTES = {
+ 'add_task': ('file_tools', 'add_task', True, ()),
+ 'agent_progress': ('agent_tools', 'agent_progress', False, ()),
+ 'all_symbols': ('code_analysis_tools', 'all_symbols', False, ()),
+ 'analyze_file': ('code_analysis_tools', 'analyze_file', True, ()),
+ 'analyze_symbol': ('code_analysis_tools', 'analyze_symbol', True, ()),
+ 'ask_user_questions': ('command_tools', 'ask_user_questions', True, ()),
+ 'backup_workspace': ('file_tools', 'backup_workspace', True, ()),
+ 'calculator': ('command_tools', 'calculator', True, ()),
+ 'check_types': ('quality_tools', 'check_types', True, ()),
+ 'code_metrics': ('code_analysis_tools', 'code_metrics', False, ()),
+ 'codebase_search': ('code_analysis_tools', 'codebase_search', True, ()),
+ 'complete_task': ('file_tools', 'complete_task', True, ()),
+ 'complexity_report': ('code_analysis_tools', 'complexity_report', False, ()),
+ 'create_file': ('file_tools', 'create_file', True, ()),
+ 'create_plan': ('agent_tools', 'create_plan', True, ()),
+ 'create_snapshot': ('file_tools', 'create_snapshot', True, ()),
+ 'delegate_subagent': ('agent_tools', 'delegate_subagent', True, ()),
+ 'delete_file': ('file_tools', 'mark_delete', True, ()),
+ 'delete_lines': ('file_tools', 'delete_lines', True, ()),
+ 'docs_lookup': ('network_tools', 'docs_lookup', True, ()),
+ 'document_symbols': ('lsp_tools', 'document_symbols', True, ()),
+ 'edit_file': ('file_tools', 'edit_file', True, ()),
+ 'execute_custom': ('command_tools', 'execute_custom', True, ()),
+ 'file_intel': ('code_analysis_tools', 'file_intel', True, ()),
+ 'find_files': ('file_tools', 'find_files', True, ()),
+ 'find_fixme': ('file_tools', 'find_markers', True, ('FIXME',)),
+ 'find_imports': ('code_analysis_tools', 'find_imports', True, ()),
+ 'find_references': ('lsp_tools', 'find_references', True, ()),
+ 'find_related': ('code_analysis_tools', 'find_related', True, ()),
+ 'find_symbol': ('file_tools', 'find_symbol', True, ()),
+ 'find_todo': ('file_tools', 'find_markers', True, ('TODO',)),
+ 'format_code': ('quality_tools', 'format_code', True, ()),
+ 'format_document': ('lsp_tools', 'format_document', True, ()),
+ 'get_diagnostics': ('lsp_tools', 'get_diagnostics', True, ()),
+ 'git_add': ('git_tools', 'git_add', True, ()),
+ 'git_branch': ('git_tools', 'git_branch', True, ()),
+ 'git_branch_create': ('git_tools', 'git_branch_create', True, ()),
+ 'git_branch_delete': ('git_tools', 'git_branch_delete', True, ()),
+ 'git_checkout': ('git_tools', 'git_checkout', True, ()),
+ 'git_commit': ('git_tools', 'git_commit', True, ()),
+ 'git_diff': ('git_tools', 'git_diff', True, ()),
+ 'git_log': ('git_tools', 'git_log', True, ()),
+ 'git_reset': ('git_tools', 'git_reset', True, ()),
+ 'git_stash': ('git_tools', 'git_stash', True, ()),
+ 'git_status': ('git_tools', 'git_status', True, ()),
+ 'goto_definition': ('lsp_tools', 'goto_definition', True, ()),
+ 'hover_info': ('lsp_tools', 'hover_info', True, ()),
+ 'impact_analysis': ('code_analysis_tools', 'impact_analysis', True, ()),
+ 'insert_lines': ('file_tools', 'insert_lines', True, ()),
+ 'lint_code': ('quality_tools', 'lint_code', True, ()),
+ 'list_files': ('file_tools', 'list_files', False, ()),
+ 'list_snapshots': ('file_tools', 'list_snapshots', False, ()),
+ 'list_uploaded_images': ('image_tools', 'list_images', True, ()),
+ 'mark_delete': ('file_tools', 'mark_delete', True, ()),
+ 'mark_step_done': ('agent_tools', 'mark_step_done', True, ()),
+ 'mcp_add': ('network_tools', 'mcp_add', True, ()),
+ 'mcp_enable': ('network_tools', 'mcp_enable', True, ()),
+ 'mcp_list': ('network_tools', 'mcp_list', True, ()),
+ 'mcp_remove': ('network_tools', 'mcp_remove', True, ()),
+ 'mcp_stop': ('network_tools', 'mcp_stop', True, ()),
+ 'modify_file': ('file_tools', 'create_file', True, ()),
+ 'parse_document': ('image_tools', 'parse_doc', True, ()),
+ 'parse_document_images': ('image_tools', 'parse_doc_images', True, ()),
+ 'parse_document_text': ('image_tools', 'parse_doc_text', True, ()),
+ 'project_brief': ('code_analysis_tools', 'project_brief', False, ()),
+ 'quick_syntax_check': ('quality_tools', 'quick_syntax_check', True, ()),
+ 'read_file': ('file_tools', 'read_file', True, ()),
+ 'rename_symbol': ('lsp_tools', 'rename_symbol', True, ()),
+ 'replace_in_files': ('file_tools', 'replace_in_files', True, ()),
+ 'replace_symbol_in_files': ('file_tools', 'rename_symbol', True, ()),
+ 'restore_snapshot': ('file_tools', 'restore_snapshot', True, ()),
+ 'revise_plan': ('agent_tools', 'revise_plan', True, ()),
+ 'risk_files': ('code_analysis_tools', 'risk_files', False, ()),
+ 'run_agents': ('agent_tools', 'run_agents', True, ()),
+ 'run_command': ('command_tools', 'run_command', True, ()),
+ 'run_command_stream': ('command_tools', 'run_command_stream', True, ()),
+ 'run_tests': ('quality_tools', 'run_tests', True, ()),
+ 'search_code': ('file_tools', 'search_code', True, ()),
+ 'shell_cd': ('command_tools', 'shell_cd', True, ()),
+ 'shell_exec': ('command_tools', 'shell_exec', True, ()),
+ 'shell_read': ('command_tools', 'shell_read', True, ()),
+ 'shell_send': ('command_tools', 'shell_send', True, ()),
+ 'shell_start': ('command_tools', 'shell_start', True, ()),
+ 'shell_stop': ('command_tools', 'shell_stop', True, ()),
+ 'show_call_chain': ('code_analysis_tools', 'show_call_chain', True, ()),
+ 'show_dependencies': ('code_analysis_tools', 'show_dependencies', False, ()),
+ 'show_plan': ('agent_tools', 'show_plan', False, ()),
+ 'sklii_call': ('sklii_tools', 'sklii_call', True, ()),
+ 'sklii_create': ('sklii_tools', 'sklii_create', True, ()),
+ 'sklii_list': ('sklii_tools', 'sklii_list', False, ()),
+ 'undo_last': ('file_tools', 'undo_last', True, ()),
+ 'update_ds_section': ('file_tools', 'update_ds_section', True, ()),
+ 'validate_all': ('quality_tools', 'validate_all', True, ()),
+ 'validate_file': ('quality_tools', 'validate_file', True, ()),
+ 'view_image': ('image_tools', 'view_image', True, ()),
+ 'web_fetch': ('network_tools', 'web_fetch', True, ()),
+ 'web_search': ('network_tools', 'web_search', True, ()),
+ }
+
def _dispatch(self, call, tool):
- """工具内部分发 — 返回 (ok, result)"""
+ """Dispatch registered tools after centralized permission and hook checks."""
try:
- # --- 文件基础操作 ---
- if tool in ("create_file", "modify_file"):
- return self.file_tools.create_file(call)
- elif tool == "read_file":
- return self.file_tools.read_file(call)
- elif tool in ("delete_file", "mark_delete"):
- return self.file_tools.mark_delete(call)
- elif tool == "list_files":
- return self.file_tools.list_files()
- elif tool == "backup_workspace":
- return self.file_tools.backup_workspace(call)
- elif tool == "update_ds_section":
- return self.file_tools.update_ds_section(call)
- elif tool == "add_task":
- return self.file_tools.add_task(call)
- elif tool == "complete_task":
- return self.file_tools.complete_task(call)
-
- # --- 网络工具 ---
- elif tool == "web_search":
- return self.network_tools.web_search(call)
- elif tool == "web_fetch":
- return self.network_tools.web_fetch(call)
- elif tool == "http_request":
+ route = self._TOOL_ROUTES.get(tool)
+ if route:
+ owner, method, takes_call, extras = route
+ args = ((call,) if takes_call else ()) + extras
+ return getattr(getattr(self, owner), method)(*args)
+ if tool == 'http_request':
result = self.network_tools.http_request(call)
if isinstance(result, dict):
- ok = bool(result.get("ok", False))
- status_code = result.get("status_code", "n/a")
- headers = result.get("headers") or {}
- body = str(result.get("body", ""))
- error = result.get("error")
- lines = ["[http_request] " + str(call.get("method", "GET")) + " " + str(call.get("url", ""))]
- lines.append("status: " + str(status_code))
+ ok = bool(result.get('ok', False))
+ status_code = result.get('status_code', 'n/a')
+ headers = result.get('headers') or {}
+ body = str(result.get('body', ''))
+ error = result.get('error')
+ lines = ['[http_request] ' + str(call.get('method', 'GET')) + ' ' + str(call.get('url', ''))]
+ lines.append('status: ' + str(status_code))
if headers:
- hs = headers.items() if hasattr(headers, "items") else []
- lines.append("headers:")
+ hs = headers.items() if hasattr(headers, 'items') else []
+ lines.append('headers:')
for k, v in list(hs)[:10]:
- lines.append(" " + str(k) + ": " + str(v)[:160])
- lines.append("body:")
+ lines.append(' ' + str(k) + ': ' + str(v)[:160])
+ lines.append('body:')
lines.append(body[:4000])
if error:
- lines.append("error: " + str(error))
- return ok, "\n".join(lines)
+ lines.append('error: ' + str(error))
+ return (ok, '\n'.join(lines))
return result
- elif tool == "docs_lookup":
- return self.network_tools.docs_lookup(call)
-
- # --- 命令执行 ---
- elif tool == "run_command":
- return self.command_tools.run_command(call)
- elif tool == "run_command_stream":
- return self.command_tools.run_command_stream(call)
- elif tool == "calculator":
- return self.command_tools.calculator(call)
- elif tool == "execute_custom":
- return self.command_tools.execute_custom(call)
-
- # --- 持久终端会话 ---
- elif tool == "shell_start":
- return self.command_tools.shell_start(call)
- elif tool == "shell_exec":
- return self.command_tools.shell_exec(call)
- elif tool == "shell_send":
- return self.command_tools.shell_send(call)
- elif tool == "shell_read":
- return self.command_tools.shell_read(call)
- elif tool == "shell_cd":
- return self.command_tools.shell_cd(call)
- elif tool == "shell_stop":
- return self.command_tools.shell_stop(call)
-
- # --- Git 工具集 ---
- elif tool == "git_status":
- return self.git_tools.git_status(call)
- elif tool == "git_diff":
- return self.git_tools.git_diff(call)
- elif tool == "git_commit":
- return self.git_tools.git_commit(call)
- elif tool == "git_add":
- return self.git_tools.git_add(call)
- elif tool == "git_branch":
- return self.git_tools.git_branch(call)
- elif tool == "git_branch_create":
- return self.git_tools.git_branch_create(call)
- elif tool == "git_branch_delete":
- return self.git_tools.git_branch_delete(call)
- elif tool == "git_log":
- return self.git_tools.git_log(call)
- elif tool == "git_checkout":
- return self.git_tools.git_checkout(call)
- elif tool == "git_stash":
- return self.git_tools.git_stash(call)
- elif tool == "git_reset":
- return self.git_tools.git_reset(call)
-
- # --- MCP / 用户提问 ---
- elif tool in ("mcp_call", "mcp_start"):
- # C-005:子代理/受限上下文禁止 MCP 命令类操作
- if self._subagent_restrictions.get("forbid_commands", False):
- logger.info("[AUDIT] denied tool=%s reason=forbid_commands", tool)
- return False, f"[{tool}] 已禁用 (子代理不可执行 MCP 命令)"
- if tool == "mcp_call":
+ if tool in ('mcp_call', 'mcp_start'):
+ if self._subagent_restrictions.get('forbid_commands', False):
+ logger.info('[AUDIT] denied tool=%s reason=forbid_commands', tool)
+ return (False, f'[{tool}] 已禁用 (子代理不可执行 MCP 命令)')
+ if tool == 'mcp_call':
return self.network_tools.mcp_call(call)
return self.network_tools.mcp_start(call)
- elif tool == "mcp_list":
- return self.network_tools.mcp_list(call)
- elif tool == "mcp_add":
- return self.network_tools.mcp_add(call)
- elif tool == "mcp_remove":
- return self.network_tools.mcp_remove(call)
- elif tool == "mcp_enable":
- return self.network_tools.mcp_enable(call)
- elif tool == "mcp_stop":
- return self.network_tools.mcp_stop(call)
- elif tool == "ask_user_questions":
- return self.command_tools.ask_user_questions(call)
- elif tool == "exit_conversation":
- return True, "[exit_conversation] " + str(call.get("reason", ""))
-
- # --- 快照 / 撤销 ---
- elif tool == "create_snapshot":
- return self.file_tools.create_snapshot(call)
- elif tool == "restore_snapshot":
- return self.file_tools.restore_snapshot(call)
- elif tool == "list_snapshots":
- return self.file_tools.list_snapshots()
- elif tool == "undo_last":
- return self.file_tools.undo_last(call)
-
- # --- 子代理 ---
- elif tool == "delegate_subagent":
- return self.agent_tools.delegate_subagent(call)
-
- # --- 子代理工具(parallel sub-agents)---
- elif tool == "start_subagent":
- return self._handle_subagent_tool("start_subagent", call)
- elif tool == "list_subagents":
- return self._handle_subagent_tool("list_subagents", call)
- elif tool == "get_subagent_results":
- return self._handle_subagent_tool("get_subagent_results", call)
- elif tool == "stop_subagent":
- return self._handle_subagent_tool("stop_subagent", call)
-
- # --- 代码搜索 ---
- elif tool == "search_code":
- return self.file_tools.search_code(call)
- elif tool == "find_symbol":
- return self.file_tools.find_symbol(call)
- elif tool == "find_files":
- return self.file_tools.find_files(call)
- elif tool == "find_todo":
- return self.file_tools.find_markers(call, "TODO")
- elif tool == "find_fixme":
- return self.file_tools.find_markers(call, "FIXME")
-
- # --- 代码编辑 ---
- elif tool == "edit_file":
- return self.file_tools.edit_file(call)
- elif tool == "insert_lines":
- return self.file_tools.insert_lines(call)
- elif tool == "delete_lines":
- return self.file_tools.delete_lines(call)
- elif tool == "replace_in_files":
- return self.file_tools.replace_in_files(call)
- elif tool == "replace_symbol_in_files":
- return self.file_tools.rename_symbol(call)
-
- # --- 代码分析 ---
- elif tool == "analyze_file":
- return self.code_analysis_tools.analyze_file(call)
- elif tool == "find_imports":
- return self.code_analysis_tools.find_imports(call)
- elif tool == "show_call_chain":
- return self.code_analysis_tools.show_call_chain(call)
- elif tool == "code_metrics":
- return self.code_analysis_tools.code_metrics()
- elif tool == "show_dependencies":
- return self.code_analysis_tools.show_dependencies()
-
- # --- 测试 / 质量 / 格式化 ---
- elif tool == "run_tests":
- return self.quality_tools.run_tests(call)
- elif tool == "lint_code":
- return self.quality_tools.lint_code(call)
- elif tool == "check_types":
- return self.quality_tools.check_types(call)
- elif tool == "format_code":
- return self.quality_tools.format_code(call)
-
- # --- 代码分析层 (CodeIntel) ---
- elif tool == "project_brief":
- return self.code_analysis_tools.project_brief()
- elif tool == "file_intel":
- return self.code_analysis_tools.file_intel(call)
- elif tool == "find_related":
- return self.code_analysis_tools.find_related(call)
- elif tool == "analyze_symbol":
- return self.code_analysis_tools.analyze_symbol(call)
- elif tool == "impact_analysis":
- return self.code_analysis_tools.impact_analysis(call)
- elif tool == "all_symbols":
- return self.code_analysis_tools.all_symbols()
- elif tool == "complexity_report":
- return self.code_analysis_tools.complexity_report()
- elif tool == "risk_files":
- return self.code_analysis_tools.risk_files()
- elif tool == "codebase_search":
- return self.code_analysis_tools.codebase_search(call)
-
- # --- LSP真实Language Server工具 ---
- elif tool == "goto_definition":
- return self.lsp_tools.goto_definition(call)
- elif tool == "find_references":
- return self.lsp_tools.find_references(call)
- elif tool == "hover_info":
- return self.lsp_tools.hover_info(call)
- elif tool == "get_diagnostics":
- return self.lsp_tools.get_diagnostics(call)
- elif tool == "document_symbols":
- return self.lsp_tools.document_symbols(call)
- elif tool == "rename_symbol":
- return self.lsp_tools.rename_symbol(call)
- elif tool == "format_document":
- return self.lsp_tools.format_document(call)
-
- # --- 自动验证 ---
- elif tool == "validate_all":
- return self.quality_tools.validate_all(call)
- elif tool == "validate_file":
- return self.quality_tools.validate_file(call)
- elif tool == "quick_syntax_check":
- return self.quality_tools.quick_syntax_check(call)
-
- # --- 多代理 ---
- elif tool == "create_plan":
- return self.agent_tools.create_plan(call)
- elif tool == "revise_plan":
- return self.agent_tools.revise_plan(call)
- elif tool == "show_plan":
- return self.agent_tools.show_plan()
- elif tool == "run_agents":
- return self.agent_tools.run_agents(call)
- elif tool == "agent_progress":
- return self.agent_tools.agent_progress()
- elif tool == "mark_step_done":
- return self.agent_tools.mark_step_done(call)
-
- # --- 图片 / 文档解析工具 ---
- elif tool == "view_image":
- return self.image_tools.view_image(call)
- elif tool == "list_uploaded_images":
- return self.image_tools.list_images(call)
- elif tool == "parse_document_text":
- return self.image_tools.parse_doc_text(call)
- elif tool == "parse_document_images":
- return self.image_tools.parse_doc_images(call)
- elif tool == "parse_document":
- return self.image_tools.parse_doc(call)
-
- # --- sklii 自定义工具包 ---
- elif tool == "sklii_call":
- return self.sklii_tools.sklii_call(call)
- elif tool == "sklii_create":
- return self.sklii_tools.sklii_create(call)
- elif tool == "sklii_list":
- return self.sklii_tools.sklii_list()
-
- # --- Lessons / Notes (Constitutional) ---
- elif tool == "record_lesson":
- return self.lesson_tools.record_lesson(
- call.get("title", ""),
- call.get("content", ""),
- call.get("tags", ""),
- )
- elif tool == "list_lessons":
- return self.lesson_tools.list_lessons(
- call.get("tag", ""),
- call.get("search", ""),
- )
- elif tool == "delete_lesson":
- return self.lesson_tools.delete_lesson(call.get("title", ""))
-
- elif tool == "set_conversation_title":
+ if tool == 'exit_conversation':
+ return (True, '[exit_conversation] ' + str(call.get('reason', '')))
+ if tool == 'start_subagent':
+ return self._handle_subagent_tool('start_subagent', call)
+ if tool == 'list_subagents':
+ return self._handle_subagent_tool('list_subagents', call)
+ if tool == 'get_subagent_results':
+ return self._handle_subagent_tool('get_subagent_results', call)
+ if tool == 'stop_subagent':
+ return self._handle_subagent_tool('stop_subagent', call)
+ if tool == 'record_lesson':
+ return self.lesson_tools.record_lesson(call.get('title', ''), call.get('content', ''), call.get('tags', ''))
+ if tool == 'list_lessons':
+ return self.lesson_tools.list_lessons(call.get('tag', ''), call.get('search', ''))
+ if tool == 'delete_lesson':
+ return self.lesson_tools.delete_lesson(call.get('title', ''))
+ if tool == 'set_conversation_title':
return self._set_conversation_title(call)
+ return False, "未知工具: " + str(tool)
+ except Exception as exc:
+ return False, "执行异常: " + str(exc)
- else:
- return False, "未知工具: " + str(tool)
- except Exception as e:
- return False, "执行异常: " + str(e)
# ========== 子代理辅助方法 ==========
def _handle_subagent_tool(self, tool_name: str, call: dict) -> tuple:
diff --git a/node_modules/rxjs/dist/cjs/internal/observable/dom/WebSocketSubject.js b/node_modules/rxjs/dist/cjs/internal/observable/dom/WebSocketSubject.js
deleted file mode 100644
index cead103..0000000
--- a/node_modules/rxjs/dist/cjs/internal/observable/dom/WebSocketSubject.js
+++ /dev/null
@@ -1,249 +0,0 @@
-"use strict";
-var __extends = (this && this.__extends) || (function () {
- var extendStatics = function (d, b) {
- extendStatics = Object.setPrototypeOf ||
- ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
- function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
- return extendStatics(d, b);
- };
- return function (d, b) {
- if (typeof b !== "function" && b !== null)
- throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
- extendStatics(d, b);
- function __() { this.constructor = d; }
- d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
- };
-})();
-var __assign = (this && this.__assign) || function () {
- __assign = Object.assign || function(t) {
- for (var s, i = 1, n = arguments.length; i < n; i++) {
- s = arguments[i];
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
- t[p] = s[p];
- }
- return t;
- };
- return __assign.apply(this, arguments);
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.WebSocketSubject = void 0;
-var Subject_1 = require("../../Subject");
-var Subscriber_1 = require("../../Subscriber");
-var Observable_1 = require("../../Observable");
-var Subscription_1 = require("../../Subscription");
-var ReplaySubject_1 = require("../../ReplaySubject");
-var DEFAULT_WEBSOCKET_CONFIG = {
- url: '',
- deserializer: function (e) { return JSON.parse(e.data); },
- serializer: function (value) { return JSON.stringify(value); },
-};
-var WEBSOCKETSUBJECT_INVALID_ERROR_OBJECT = 'WebSocketSubject.error must be called with an object with an error code, and an optional reason: { code: number, reason: string }';
-var WebSocketSubject = (function (_super) {
- __extends(WebSocketSubject, _super);
- function WebSocketSubject(urlConfigOrSource, destination) {
- var _this = _super.call(this) || this;
- _this._socket = null;
- if (urlConfigOrSource instanceof Observable_1.Observable) {
- _this.destination = destination;
- _this.source = urlConfigOrSource;
- }
- else {
- var config = (_this._config = __assign({}, DEFAULT_WEBSOCKET_CONFIG));
- _this._output = new Subject_1.Subject();
- if (typeof urlConfigOrSource === 'string') {
- config.url = urlConfigOrSource;
- }
- else {
- for (var key in urlConfigOrSource) {
- if (urlConfigOrSource.hasOwnProperty(key)) {
- config[key] = urlConfigOrSource[key];
- }
- }
- }
- if (!config.WebSocketCtor && WebSocket) {
- config.WebSocketCtor = WebSocket;
- }
- else if (!config.WebSocketCtor) {
- throw new Error('no WebSocket constructor can be found');
- }
- _this.destination = new ReplaySubject_1.ReplaySubject();
- }
- return _this;
- }
- WebSocketSubject.prototype.lift = function (operator) {
- var sock = new WebSocketSubject(this._config, this.destination);
- sock.operator = operator;
- sock.source = this;
- return sock;
- };
- WebSocketSubject.prototype._resetState = function () {
- this._socket = null;
- if (!this.source) {
- this.destination = new ReplaySubject_1.ReplaySubject();
- }
- this._output = new Subject_1.Subject();
- };
- WebSocketSubject.prototype.multiplex = function (subMsg, unsubMsg, messageFilter) {
- var self = this;
- return new Observable_1.Observable(function (observer) {
- try {
- self.next(subMsg());
- }
- catch (err) {
- observer.error(err);
- }
- var subscription = self.subscribe({
- next: function (x) {
- try {
- if (messageFilter(x)) {
- observer.next(x);
- }
- }
- catch (err) {
- observer.error(err);
- }
- },
- error: function (err) { return observer.error(err); },
- complete: function () { return observer.complete(); },
- });
- return function () {
- try {
- self.next(unsubMsg());
- }
- catch (err) {
- observer.error(err);
- }
- subscription.unsubscribe();
- };
- });
- };
- WebSocketSubject.prototype._connectSocket = function () {
- var _this = this;
- var _a = this._config, WebSocketCtor = _a.WebSocketCtor, protocol = _a.protocol, url = _a.url, binaryType = _a.binaryType;
- var observer = this._output;
- var socket = null;
- try {
- socket = protocol ? new WebSocketCtor(url, protocol) : new WebSocketCtor(url);
- this._socket = socket;
- if (binaryType) {
- this._socket.binaryType = binaryType;
- }
- }
- catch (e) {
- observer.error(e);
- return;
- }
- var subscription = new Subscription_1.Subscription(function () {
- _this._socket = null;
- if (socket && socket.readyState === 1) {
- socket.close();
- }
- });
- socket.onopen = function (evt) {
- var _socket = _this._socket;
- if (!_socket) {
- socket.close();
- _this._resetState();
- return;
- }
- var openObserver = _this._config.openObserver;
- if (openObserver) {
- openObserver.next(evt);
- }
- var queue = _this.destination;
- _this.destination = Subscriber_1.Subscriber.create(function (x) {
- if (socket.readyState === 1) {
- try {
- var serializer = _this._config.serializer;
- socket.send(serializer(x));
- }
- catch (e) {
- _this.destination.error(e);
- }
- }
- }, function (err) {
- var closingObserver = _this._config.closingObserver;
- if (closingObserver) {
- closingObserver.next(undefined);
- }
- if (err && err.code) {
- socket.close(err.code, err.reason);
- }
- else {
- observer.error(new TypeError(WEBSOCKETSUBJECT_INVALID_ERROR_OBJECT));
- }
- _this._resetState();
- }, function () {
- var closingObserver = _this._config.closingObserver;
- if (closingObserver) {
- closingObserver.next(undefined);
- }
- socket.close();
- _this._resetState();
- });
- if (queue && queue instanceof ReplaySubject_1.ReplaySubject) {
- subscription.add(queue.subscribe(_this.destination));
- }
- };
- socket.onerror = function (e) {
- _this._resetState();
- observer.error(e);
- };
- socket.onclose = function (e) {
- if (socket === _this._socket) {
- _this._resetState();
- }
- var closeObserver = _this._config.closeObserver;
- if (closeObserver) {
- closeObserver.next(e);
- }
- if (e.wasClean) {
- observer.complete();
- }
- else {
- observer.error(e);
- }
- };
- socket.onmessage = function (e) {
- try {
- var deserializer = _this._config.deserializer;
- observer.next(deserializer(e));
- }
- catch (err) {
- observer.error(err);
- }
- };
- };
- WebSocketSubject.prototype._subscribe = function (subscriber) {
- var _this = this;
- var source = this.source;
- if (source) {
- return source.subscribe(subscriber);
- }
- if (!this._socket) {
- this._connectSocket();
- }
- this._output.subscribe(subscriber);
- subscriber.add(function () {
- var _socket = _this._socket;
- if (_this._output.observers.length === 0) {
- if (_socket && (_socket.readyState === 1 || _socket.readyState === 0)) {
- _socket.close();
- }
- _this._resetState();
- }
- });
- return subscriber;
- };
- WebSocketSubject.prototype.unsubscribe = function () {
- var _socket = this._socket;
- if (_socket && (_socket.readyState === 1 || _socket.readyState === 0)) {
- _socket.close();
- }
- this._resetState();
- _super.prototype.unsubscribe.call(this);
- };
- return WebSocketSubject;
-}(Subject_1.AnonymousSubject));
-exports.WebSocketSubject = WebSocketSubject;
-//# sourceMappingURL=WebSocketSubject.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/cjs/internal/observable/dom/WebSocketSubject.js.map b/node_modules/rxjs/dist/cjs/internal/observable/dom/WebSocketSubject.js.map
deleted file mode 100644
index ec11b0b..0000000
--- a/node_modules/rxjs/dist/cjs/internal/observable/dom/WebSocketSubject.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"WebSocketSubject.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/WebSocketSubject.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/cjs/internal/observable/dom/animationFrames.js b/node_modules/rxjs/dist/cjs/internal/observable/dom/animationFrames.js
deleted file mode 100644
index aeb24d1..0000000
--- a/node_modules/rxjs/dist/cjs/internal/observable/dom/animationFrames.js
+++ /dev/null
@@ -1,38 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.animationFrames = void 0;
-var Observable_1 = require("../../Observable");
-var performanceTimestampProvider_1 = require("../../scheduler/performanceTimestampProvider");
-var animationFrameProvider_1 = require("../../scheduler/animationFrameProvider");
-function animationFrames(timestampProvider) {
- return timestampProvider ? animationFramesFactory(timestampProvider) : DEFAULT_ANIMATION_FRAMES;
-}
-exports.animationFrames = animationFrames;
-function animationFramesFactory(timestampProvider) {
- return new Observable_1.Observable(function (subscriber) {
- var provider = timestampProvider || performanceTimestampProvider_1.performanceTimestampProvider;
- var start = provider.now();
- var id = 0;
- var run = function () {
- if (!subscriber.closed) {
- id = animationFrameProvider_1.animationFrameProvider.requestAnimationFrame(function (timestamp) {
- id = 0;
- var now = provider.now();
- subscriber.next({
- timestamp: timestampProvider ? now : timestamp,
- elapsed: now - start,
- });
- run();
- });
- }
- };
- run();
- return function () {
- if (id) {
- animationFrameProvider_1.animationFrameProvider.cancelAnimationFrame(id);
- }
- };
- });
-}
-var DEFAULT_ANIMATION_FRAMES = animationFramesFactory();
-//# sourceMappingURL=animationFrames.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/cjs/internal/observable/dom/animationFrames.js.map b/node_modules/rxjs/dist/cjs/internal/observable/dom/animationFrames.js.map
deleted file mode 100644
index 3291a02..0000000
--- a/node_modules/rxjs/dist/cjs/internal/observable/dom/animationFrames.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"animationFrames.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/animationFrames.ts"],"names":[],"mappings":";;;AAAA,+CAA8C;AAE9C,6FAA4F;AAC5F,iFAAgF;AAuEhF,SAAgB,eAAe,CAAC,iBAAqC;IACnE,OAAO,iBAAiB,CAAC,CAAC,CAAC,sBAAsB,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC;AAClG,CAAC;AAFD,0CAEC;AAMD,SAAS,sBAAsB,CAAC,iBAAqC;IACnE,OAAO,IAAI,uBAAU,CAAyC,UAAC,UAAU;QAIvE,IAAM,QAAQ,GAAG,iBAAiB,IAAI,2DAA4B,CAAC;QAMnE,IAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;QAE7B,IAAI,EAAE,GAAG,CAAC,CAAC;QACX,IAAM,GAAG,GAAG;YACV,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE;gBACtB,EAAE,GAAG,+CAAsB,CAAC,qBAAqB,CAAC,UAAC,SAAuC;oBACxF,EAAE,GAAG,CAAC,CAAC;oBAQP,IAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;oBAC3B,UAAU,CAAC,IAAI,CAAC;wBACd,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;wBAC9C,OAAO,EAAE,GAAG,GAAG,KAAK;qBACrB,CAAC,CAAC;oBACH,GAAG,EAAE,CAAC;gBACR,CAAC,CAAC,CAAC;aACJ;QACH,CAAC,CAAC;QAEF,GAAG,EAAE,CAAC;QAEN,OAAO;YACL,IAAI,EAAE,EAAE;gBACN,+CAAsB,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC;aACjD;QACH,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAMD,IAAM,wBAAwB,GAAG,sBAAsB,EAAE,CAAC"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/cjs/internal/observable/dom/fetch.js b/node_modules/rxjs/dist/cjs/internal/observable/dom/fetch.js
deleted file mode 100644
index 0bb09ef..0000000
--- a/node_modules/rxjs/dist/cjs/internal/observable/dom/fetch.js
+++ /dev/null
@@ -1,79 +0,0 @@
-"use strict";
-var __assign = (this && this.__assign) || function () {
- __assign = Object.assign || function(t) {
- for (var s, i = 1, n = arguments.length; i < n; i++) {
- s = arguments[i];
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
- t[p] = s[p];
- }
- return t;
- };
- return __assign.apply(this, arguments);
-};
-var __rest = (this && this.__rest) || function (s, e) {
- var t = {};
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
- t[p] = s[p];
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
- t[p[i]] = s[p[i]];
- }
- return t;
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.fromFetch = void 0;
-var OperatorSubscriber_1 = require("../../operators/OperatorSubscriber");
-var Observable_1 = require("../../Observable");
-var innerFrom_1 = require("../../observable/innerFrom");
-function fromFetch(input, initWithSelector) {
- if (initWithSelector === void 0) { initWithSelector = {}; }
- var selector = initWithSelector.selector, init = __rest(initWithSelector, ["selector"]);
- return new Observable_1.Observable(function (subscriber) {
- var controller = new AbortController();
- var signal = controller.signal;
- var abortable = true;
- var outerSignal = init.signal;
- if (outerSignal) {
- if (outerSignal.aborted) {
- controller.abort();
- }
- else {
- var outerSignalHandler_1 = function () {
- if (!signal.aborted) {
- controller.abort();
- }
- };
- outerSignal.addEventListener('abort', outerSignalHandler_1);
- subscriber.add(function () { return outerSignal.removeEventListener('abort', outerSignalHandler_1); });
- }
- }
- var perSubscriberInit = __assign(__assign({}, init), { signal: signal });
- var handleError = function (err) {
- abortable = false;
- subscriber.error(err);
- };
- fetch(input, perSubscriberInit)
- .then(function (response) {
- if (selector) {
- innerFrom_1.innerFrom(selector(response)).subscribe(OperatorSubscriber_1.createOperatorSubscriber(subscriber, undefined, function () {
- abortable = false;
- subscriber.complete();
- }, handleError));
- }
- else {
- abortable = false;
- subscriber.next(response);
- subscriber.complete();
- }
- })
- .catch(handleError);
- return function () {
- if (abortable) {
- controller.abort();
- }
- };
- });
-}
-exports.fromFetch = fromFetch;
-//# sourceMappingURL=fetch.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/cjs/internal/observable/dom/fetch.js.map b/node_modules/rxjs/dist/cjs/internal/observable/dom/fetch.js.map
deleted file mode 100644
index a770094..0000000
--- a/node_modules/rxjs/dist/cjs/internal/observable/dom/fetch.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/fetch.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAAA,yEAA8E;AAC9E,+CAA8C;AAC9C,wDAAuD;AA4FvD,SAAgB,SAAS,CACvB,KAAuB,EACvB,gBAEM;IAFN,iCAAA,EAAA,qBAEM;IAEE,IAAA,QAAQ,GAAc,gBAAgB,SAA9B,EAAK,IAAI,UAAK,gBAAgB,EAAxC,YAAqB,CAAF,CAAsB;IAC/C,OAAO,IAAI,uBAAU,CAAe,UAAC,UAAU;QAK7C,IAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACjC,IAAA,MAAM,GAAK,UAAU,OAAf,CAAgB;QAK9B,IAAI,SAAS,GAAG,IAAI,CAAC;QAKb,IAAQ,WAAW,GAAK,IAAI,OAAT,CAAU;QACrC,IAAI,WAAW,EAAE;YACf,IAAI,WAAW,CAAC,OAAO,EAAE;gBACvB,UAAU,CAAC,KAAK,EAAE,CAAC;aACpB;iBAAM;gBAGL,IAAM,oBAAkB,GAAG;oBACzB,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE;wBACnB,UAAU,CAAC,KAAK,EAAE,CAAC;qBACpB;gBACH,CAAC,CAAC;gBACF,WAAW,CAAC,gBAAgB,CAAC,OAAO,EAAE,oBAAkB,CAAC,CAAC;gBAC1D,UAAU,CAAC,GAAG,CAAC,cAAM,OAAA,WAAW,CAAC,mBAAmB,CAAC,OAAO,EAAE,oBAAkB,CAAC,EAA5D,CAA4D,CAAC,CAAC;aACpF;SACF;QAOD,IAAM,iBAAiB,yBAAqB,IAAI,KAAE,MAAM,QAAA,GAAE,CAAC;QAE3D,IAAM,WAAW,GAAG,UAAC,GAAQ;YAC3B,SAAS,GAAG,KAAK,CAAC;YAClB,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC,CAAC;QAEF,KAAK,CAAC,KAAK,EAAE,iBAAiB,CAAC;aAC5B,IAAI,CAAC,UAAC,QAAQ;YACb,IAAI,QAAQ,EAAE;gBAIZ,qBAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CACrC,6CAAwB,CACtB,UAAU,EAEV,SAAS,EAET;oBACE,SAAS,GAAG,KAAK,CAAC;oBAClB,UAAU,CAAC,QAAQ,EAAE,CAAC;gBACxB,CAAC,EACD,WAAW,CACZ,CACF,CAAC;aACH;iBAAM;gBACL,SAAS,GAAG,KAAK,CAAC;gBAClB,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBAC1B,UAAU,CAAC,QAAQ,EAAE,CAAC;aACvB;QACH,CAAC,CAAC;aACD,KAAK,CAAC,WAAW,CAAC,CAAC;QAEtB,OAAO;YACL,IAAI,SAAS,EAAE;gBACb,UAAU,CAAC,KAAK,EAAE,CAAC;aACpB;QACH,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AArFD,8BAqFC"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/cjs/internal/observable/dom/webSocket.js b/node_modules/rxjs/dist/cjs/internal/observable/dom/webSocket.js
deleted file mode 100644
index 449b942..0000000
--- a/node_modules/rxjs/dist/cjs/internal/observable/dom/webSocket.js
+++ /dev/null
@@ -1,9 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.webSocket = void 0;
-var WebSocketSubject_1 = require("./WebSocketSubject");
-function webSocket(urlConfigOrSource) {
- return new WebSocketSubject_1.WebSocketSubject(urlConfigOrSource);
-}
-exports.webSocket = webSocket;
-//# sourceMappingURL=webSocket.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/cjs/internal/observable/dom/webSocket.js.map b/node_modules/rxjs/dist/cjs/internal/observable/dom/webSocket.js.map
deleted file mode 100644
index 2cb3f6b..0000000
--- a/node_modules/rxjs/dist/cjs/internal/observable/dom/webSocket.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"webSocket.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/webSocket.ts"],"names":[],"mappings":";;;AAAA,uDAA8E;AA8J9E,SAAgB,SAAS,CAAI,iBAAqD;IAChF,OAAO,IAAI,mCAAgB,CAAI,iBAAiB,CAAC,CAAC;AACpD,CAAC;AAFD,8BAEC"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm/internal/observable/dom/WebSocketSubject.js b/node_modules/rxjs/dist/esm/internal/observable/dom/WebSocketSubject.js
deleted file mode 100644
index 7a61722..0000000
--- a/node_modules/rxjs/dist/esm/internal/observable/dom/WebSocketSubject.js
+++ /dev/null
@@ -1,214 +0,0 @@
-import { Subject, AnonymousSubject } from '../../Subject';
-import { Subscriber } from '../../Subscriber';
-import { Observable } from '../../Observable';
-import { Subscription } from '../../Subscription';
-import { ReplaySubject } from '../../ReplaySubject';
-const DEFAULT_WEBSOCKET_CONFIG = {
- url: '',
- deserializer: (e) => JSON.parse(e.data),
- serializer: (value) => JSON.stringify(value),
-};
-const WEBSOCKETSUBJECT_INVALID_ERROR_OBJECT = 'WebSocketSubject.error must be called with an object with an error code, and an optional reason: { code: number, reason: string }';
-export class WebSocketSubject extends AnonymousSubject {
- constructor(urlConfigOrSource, destination) {
- super();
- this._socket = null;
- if (urlConfigOrSource instanceof Observable) {
- this.destination = destination;
- this.source = urlConfigOrSource;
- }
- else {
- const config = (this._config = Object.assign({}, DEFAULT_WEBSOCKET_CONFIG));
- this._output = new Subject();
- if (typeof urlConfigOrSource === 'string') {
- config.url = urlConfigOrSource;
- }
- else {
- for (const key in urlConfigOrSource) {
- if (urlConfigOrSource.hasOwnProperty(key)) {
- config[key] = urlConfigOrSource[key];
- }
- }
- }
- if (!config.WebSocketCtor && WebSocket) {
- config.WebSocketCtor = WebSocket;
- }
- else if (!config.WebSocketCtor) {
- throw new Error('no WebSocket constructor can be found');
- }
- this.destination = new ReplaySubject();
- }
- }
- lift(operator) {
- const sock = new WebSocketSubject(this._config, this.destination);
- sock.operator = operator;
- sock.source = this;
- return sock;
- }
- _resetState() {
- this._socket = null;
- if (!this.source) {
- this.destination = new ReplaySubject();
- }
- this._output = new Subject();
- }
- multiplex(subMsg, unsubMsg, messageFilter) {
- const self = this;
- return new Observable((observer) => {
- try {
- self.next(subMsg());
- }
- catch (err) {
- observer.error(err);
- }
- const subscription = self.subscribe({
- next: (x) => {
- try {
- if (messageFilter(x)) {
- observer.next(x);
- }
- }
- catch (err) {
- observer.error(err);
- }
- },
- error: (err) => observer.error(err),
- complete: () => observer.complete(),
- });
- return () => {
- try {
- self.next(unsubMsg());
- }
- catch (err) {
- observer.error(err);
- }
- subscription.unsubscribe();
- };
- });
- }
- _connectSocket() {
- const { WebSocketCtor, protocol, url, binaryType } = this._config;
- const observer = this._output;
- let socket = null;
- try {
- socket = protocol ? new WebSocketCtor(url, protocol) : new WebSocketCtor(url);
- this._socket = socket;
- if (binaryType) {
- this._socket.binaryType = binaryType;
- }
- }
- catch (e) {
- observer.error(e);
- return;
- }
- const subscription = new Subscription(() => {
- this._socket = null;
- if (socket && socket.readyState === 1) {
- socket.close();
- }
- });
- socket.onopen = (evt) => {
- const { _socket } = this;
- if (!_socket) {
- socket.close();
- this._resetState();
- return;
- }
- const { openObserver } = this._config;
- if (openObserver) {
- openObserver.next(evt);
- }
- const queue = this.destination;
- this.destination = Subscriber.create((x) => {
- if (socket.readyState === 1) {
- try {
- const { serializer } = this._config;
- socket.send(serializer(x));
- }
- catch (e) {
- this.destination.error(e);
- }
- }
- }, (err) => {
- const { closingObserver } = this._config;
- if (closingObserver) {
- closingObserver.next(undefined);
- }
- if (err && err.code) {
- socket.close(err.code, err.reason);
- }
- else {
- observer.error(new TypeError(WEBSOCKETSUBJECT_INVALID_ERROR_OBJECT));
- }
- this._resetState();
- }, () => {
- const { closingObserver } = this._config;
- if (closingObserver) {
- closingObserver.next(undefined);
- }
- socket.close();
- this._resetState();
- });
- if (queue && queue instanceof ReplaySubject) {
- subscription.add(queue.subscribe(this.destination));
- }
- };
- socket.onerror = (e) => {
- this._resetState();
- observer.error(e);
- };
- socket.onclose = (e) => {
- if (socket === this._socket) {
- this._resetState();
- }
- const { closeObserver } = this._config;
- if (closeObserver) {
- closeObserver.next(e);
- }
- if (e.wasClean) {
- observer.complete();
- }
- else {
- observer.error(e);
- }
- };
- socket.onmessage = (e) => {
- try {
- const { deserializer } = this._config;
- observer.next(deserializer(e));
- }
- catch (err) {
- observer.error(err);
- }
- };
- }
- _subscribe(subscriber) {
- const { source } = this;
- if (source) {
- return source.subscribe(subscriber);
- }
- if (!this._socket) {
- this._connectSocket();
- }
- this._output.subscribe(subscriber);
- subscriber.add(() => {
- const { _socket } = this;
- if (this._output.observers.length === 0) {
- if (_socket && (_socket.readyState === 1 || _socket.readyState === 0)) {
- _socket.close();
- }
- this._resetState();
- }
- });
- return subscriber;
- }
- unsubscribe() {
- const { _socket } = this;
- if (_socket && (_socket.readyState === 1 || _socket.readyState === 0)) {
- _socket.close();
- }
- this._resetState();
- super.unsubscribe();
- }
-}
-//# sourceMappingURL=WebSocketSubject.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm/internal/observable/dom/WebSocketSubject.js.map b/node_modules/rxjs/dist/esm/internal/observable/dom/WebSocketSubject.js.map
deleted file mode 100644
index 0a1ef94..0000000
--- a/node_modules/rxjs/dist/esm/internal/observable/dom/WebSocketSubject.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"WebSocketSubject.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/WebSocketSubject.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm/internal/observable/dom/animationFrames.js b/node_modules/rxjs/dist/esm/internal/observable/dom/animationFrames.js
deleted file mode 100644
index d29825f..0000000
--- a/node_modules/rxjs/dist/esm/internal/observable/dom/animationFrames.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Observable } from '../../Observable';
-import { performanceTimestampProvider } from '../../scheduler/performanceTimestampProvider';
-import { animationFrameProvider } from '../../scheduler/animationFrameProvider';
-export function animationFrames(timestampProvider) {
- return timestampProvider ? animationFramesFactory(timestampProvider) : DEFAULT_ANIMATION_FRAMES;
-}
-function animationFramesFactory(timestampProvider) {
- return new Observable((subscriber) => {
- const provider = timestampProvider || performanceTimestampProvider;
- const start = provider.now();
- let id = 0;
- const run = () => {
- if (!subscriber.closed) {
- id = animationFrameProvider.requestAnimationFrame((timestamp) => {
- id = 0;
- const now = provider.now();
- subscriber.next({
- timestamp: timestampProvider ? now : timestamp,
- elapsed: now - start,
- });
- run();
- });
- }
- };
- run();
- return () => {
- if (id) {
- animationFrameProvider.cancelAnimationFrame(id);
- }
- };
- });
-}
-const DEFAULT_ANIMATION_FRAMES = animationFramesFactory();
-//# sourceMappingURL=animationFrames.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm/internal/observable/dom/animationFrames.js.map b/node_modules/rxjs/dist/esm/internal/observable/dom/animationFrames.js.map
deleted file mode 100644
index 56c16cd..0000000
--- a/node_modules/rxjs/dist/esm/internal/observable/dom/animationFrames.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"animationFrames.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/animationFrames.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,4BAA4B,EAAE,MAAM,8CAA8C,CAAC;AAC5F,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAuEhF,MAAM,UAAU,eAAe,CAAC,iBAAqC;IACnE,OAAO,iBAAiB,CAAC,CAAC,CAAC,sBAAsB,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC;AAClG,CAAC;AAMD,SAAS,sBAAsB,CAAC,iBAAqC;IACnE,OAAO,IAAI,UAAU,CAAyC,CAAC,UAAU,EAAE,EAAE;QAI3E,MAAM,QAAQ,GAAG,iBAAiB,IAAI,4BAA4B,CAAC;QAMnE,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;QAE7B,IAAI,EAAE,GAAG,CAAC,CAAC;QACX,MAAM,GAAG,GAAG,GAAG,EAAE;YACf,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE;gBACtB,EAAE,GAAG,sBAAsB,CAAC,qBAAqB,CAAC,CAAC,SAAuC,EAAE,EAAE;oBAC5F,EAAE,GAAG,CAAC,CAAC;oBAQP,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;oBAC3B,UAAU,CAAC,IAAI,CAAC;wBACd,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;wBAC9C,OAAO,EAAE,GAAG,GAAG,KAAK;qBACrB,CAAC,CAAC;oBACH,GAAG,EAAE,CAAC;gBACR,CAAC,CAAC,CAAC;aACJ;QACH,CAAC,CAAC;QAEF,GAAG,EAAE,CAAC;QAEN,OAAO,GAAG,EAAE;YACV,IAAI,EAAE,EAAE;gBACN,sBAAsB,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC;aACjD;QACH,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAMD,MAAM,wBAAwB,GAAG,sBAAsB,EAAE,CAAC"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm/internal/observable/dom/fetch.js b/node_modules/rxjs/dist/esm/internal/observable/dom/fetch.js
deleted file mode 100644
index 48b0af3..0000000
--- a/node_modules/rxjs/dist/esm/internal/observable/dom/fetch.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import { __rest } from "tslib";
-import { createOperatorSubscriber } from '../../operators/OperatorSubscriber';
-import { Observable } from '../../Observable';
-import { innerFrom } from '../../observable/innerFrom';
-export function fromFetch(input, initWithSelector = {}) {
- const { selector } = initWithSelector, init = __rest(initWithSelector, ["selector"]);
- return new Observable((subscriber) => {
- const controller = new AbortController();
- const { signal } = controller;
- let abortable = true;
- const { signal: outerSignal } = init;
- if (outerSignal) {
- if (outerSignal.aborted) {
- controller.abort();
- }
- else {
- const outerSignalHandler = () => {
- if (!signal.aborted) {
- controller.abort();
- }
- };
- outerSignal.addEventListener('abort', outerSignalHandler);
- subscriber.add(() => outerSignal.removeEventListener('abort', outerSignalHandler));
- }
- }
- const perSubscriberInit = Object.assign(Object.assign({}, init), { signal });
- const handleError = (err) => {
- abortable = false;
- subscriber.error(err);
- };
- fetch(input, perSubscriberInit)
- .then((response) => {
- if (selector) {
- innerFrom(selector(response)).subscribe(createOperatorSubscriber(subscriber, undefined, () => {
- abortable = false;
- subscriber.complete();
- }, handleError));
- }
- else {
- abortable = false;
- subscriber.next(response);
- subscriber.complete();
- }
- })
- .catch(handleError);
- return () => {
- if (abortable) {
- controller.abort();
- }
- };
- });
-}
-//# sourceMappingURL=fetch.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm/internal/observable/dom/fetch.js.map b/node_modules/rxjs/dist/esm/internal/observable/dom/fetch.js.map
deleted file mode 100644
index 1418334..0000000
--- a/node_modules/rxjs/dist/esm/internal/observable/dom/fetch.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/fetch.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AA4FvD,MAAM,UAAU,SAAS,CACvB,KAAuB,EACvB,mBAEI,EAAE;IAEN,MAAM,EAAE,QAAQ,KAAc,gBAAgB,EAAzB,IAAI,UAAK,gBAAgB,EAAxC,YAAqB,CAAmB,CAAC;IAC/C,OAAO,IAAI,UAAU,CAAe,CAAC,UAAU,EAAE,EAAE;QAKjD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC;QAK9B,IAAI,SAAS,GAAG,IAAI,CAAC;QAKrB,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;QACrC,IAAI,WAAW,EAAE;YACf,IAAI,WAAW,CAAC,OAAO,EAAE;gBACvB,UAAU,CAAC,KAAK,EAAE,CAAC;aACpB;iBAAM;gBAGL,MAAM,kBAAkB,GAAG,GAAG,EAAE;oBAC9B,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE;wBACnB,UAAU,CAAC,KAAK,EAAE,CAAC;qBACpB;gBACH,CAAC,CAAC;gBACF,WAAW,CAAC,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC;gBAC1D,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,mBAAmB,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC;aACpF;SACF;QAOD,MAAM,iBAAiB,mCAAqB,IAAI,KAAE,MAAM,GAAE,CAAC;QAE3D,MAAM,WAAW,GAAG,CAAC,GAAQ,EAAE,EAAE;YAC/B,SAAS,GAAG,KAAK,CAAC;YAClB,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC,CAAC;QAEF,KAAK,CAAC,KAAK,EAAE,iBAAiB,CAAC;aAC5B,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;YACjB,IAAI,QAAQ,EAAE;gBAIZ,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CACrC,wBAAwB,CACtB,UAAU,EAEV,SAAS,EAET,GAAG,EAAE;oBACH,SAAS,GAAG,KAAK,CAAC;oBAClB,UAAU,CAAC,QAAQ,EAAE,CAAC;gBACxB,CAAC,EACD,WAAW,CACZ,CACF,CAAC;aACH;iBAAM;gBACL,SAAS,GAAG,KAAK,CAAC;gBAClB,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBAC1B,UAAU,CAAC,QAAQ,EAAE,CAAC;aACvB;QACH,CAAC,CAAC;aACD,KAAK,CAAC,WAAW,CAAC,CAAC;QAEtB,OAAO,GAAG,EAAE;YACV,IAAI,SAAS,EAAE;gBACb,UAAU,CAAC,KAAK,EAAE,CAAC;aACpB;QACH,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm/internal/observable/dom/webSocket.js b/node_modules/rxjs/dist/esm/internal/observable/dom/webSocket.js
deleted file mode 100644
index 73a51ab..0000000
--- a/node_modules/rxjs/dist/esm/internal/observable/dom/webSocket.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { WebSocketSubject } from './WebSocketSubject';
-export function webSocket(urlConfigOrSource) {
- return new WebSocketSubject(urlConfigOrSource);
-}
-//# sourceMappingURL=webSocket.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm/internal/observable/dom/webSocket.js.map b/node_modules/rxjs/dist/esm/internal/observable/dom/webSocket.js.map
deleted file mode 100644
index f284743..0000000
--- a/node_modules/rxjs/dist/esm/internal/observable/dom/webSocket.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"webSocket.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/webSocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAA0B,MAAM,oBAAoB,CAAC;AA8J9E,MAAM,UAAU,SAAS,CAAI,iBAAqD;IAChF,OAAO,IAAI,gBAAgB,CAAI,iBAAiB,CAAC,CAAC;AACpD,CAAC"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm5/internal/observable/dom/WebSocketSubject.js b/node_modules/rxjs/dist/esm5/internal/observable/dom/WebSocketSubject.js
deleted file mode 100644
index cbce16e..0000000
--- a/node_modules/rxjs/dist/esm5/internal/observable/dom/WebSocketSubject.js
+++ /dev/null
@@ -1,221 +0,0 @@
-import { __assign, __extends } from "tslib";
-import { Subject, AnonymousSubject } from '../../Subject';
-import { Subscriber } from '../../Subscriber';
-import { Observable } from '../../Observable';
-import { Subscription } from '../../Subscription';
-import { ReplaySubject } from '../../ReplaySubject';
-var DEFAULT_WEBSOCKET_CONFIG = {
- url: '',
- deserializer: function (e) { return JSON.parse(e.data); },
- serializer: function (value) { return JSON.stringify(value); },
-};
-var WEBSOCKETSUBJECT_INVALID_ERROR_OBJECT = 'WebSocketSubject.error must be called with an object with an error code, and an optional reason: { code: number, reason: string }';
-var WebSocketSubject = (function (_super) {
- __extends(WebSocketSubject, _super);
- function WebSocketSubject(urlConfigOrSource, destination) {
- var _this = _super.call(this) || this;
- _this._socket = null;
- if (urlConfigOrSource instanceof Observable) {
- _this.destination = destination;
- _this.source = urlConfigOrSource;
- }
- else {
- var config = (_this._config = __assign({}, DEFAULT_WEBSOCKET_CONFIG));
- _this._output = new Subject();
- if (typeof urlConfigOrSource === 'string') {
- config.url = urlConfigOrSource;
- }
- else {
- for (var key in urlConfigOrSource) {
- if (urlConfigOrSource.hasOwnProperty(key)) {
- config[key] = urlConfigOrSource[key];
- }
- }
- }
- if (!config.WebSocketCtor && WebSocket) {
- config.WebSocketCtor = WebSocket;
- }
- else if (!config.WebSocketCtor) {
- throw new Error('no WebSocket constructor can be found');
- }
- _this.destination = new ReplaySubject();
- }
- return _this;
- }
- WebSocketSubject.prototype.lift = function (operator) {
- var sock = new WebSocketSubject(this._config, this.destination);
- sock.operator = operator;
- sock.source = this;
- return sock;
- };
- WebSocketSubject.prototype._resetState = function () {
- this._socket = null;
- if (!this.source) {
- this.destination = new ReplaySubject();
- }
- this._output = new Subject();
- };
- WebSocketSubject.prototype.multiplex = function (subMsg, unsubMsg, messageFilter) {
- var self = this;
- return new Observable(function (observer) {
- try {
- self.next(subMsg());
- }
- catch (err) {
- observer.error(err);
- }
- var subscription = self.subscribe({
- next: function (x) {
- try {
- if (messageFilter(x)) {
- observer.next(x);
- }
- }
- catch (err) {
- observer.error(err);
- }
- },
- error: function (err) { return observer.error(err); },
- complete: function () { return observer.complete(); },
- });
- return function () {
- try {
- self.next(unsubMsg());
- }
- catch (err) {
- observer.error(err);
- }
- subscription.unsubscribe();
- };
- });
- };
- WebSocketSubject.prototype._connectSocket = function () {
- var _this = this;
- var _a = this._config, WebSocketCtor = _a.WebSocketCtor, protocol = _a.protocol, url = _a.url, binaryType = _a.binaryType;
- var observer = this._output;
- var socket = null;
- try {
- socket = protocol ? new WebSocketCtor(url, protocol) : new WebSocketCtor(url);
- this._socket = socket;
- if (binaryType) {
- this._socket.binaryType = binaryType;
- }
- }
- catch (e) {
- observer.error(e);
- return;
- }
- var subscription = new Subscription(function () {
- _this._socket = null;
- if (socket && socket.readyState === 1) {
- socket.close();
- }
- });
- socket.onopen = function (evt) {
- var _socket = _this._socket;
- if (!_socket) {
- socket.close();
- _this._resetState();
- return;
- }
- var openObserver = _this._config.openObserver;
- if (openObserver) {
- openObserver.next(evt);
- }
- var queue = _this.destination;
- _this.destination = Subscriber.create(function (x) {
- if (socket.readyState === 1) {
- try {
- var serializer = _this._config.serializer;
- socket.send(serializer(x));
- }
- catch (e) {
- _this.destination.error(e);
- }
- }
- }, function (err) {
- var closingObserver = _this._config.closingObserver;
- if (closingObserver) {
- closingObserver.next(undefined);
- }
- if (err && err.code) {
- socket.close(err.code, err.reason);
- }
- else {
- observer.error(new TypeError(WEBSOCKETSUBJECT_INVALID_ERROR_OBJECT));
- }
- _this._resetState();
- }, function () {
- var closingObserver = _this._config.closingObserver;
- if (closingObserver) {
- closingObserver.next(undefined);
- }
- socket.close();
- _this._resetState();
- });
- if (queue && queue instanceof ReplaySubject) {
- subscription.add(queue.subscribe(_this.destination));
- }
- };
- socket.onerror = function (e) {
- _this._resetState();
- observer.error(e);
- };
- socket.onclose = function (e) {
- if (socket === _this._socket) {
- _this._resetState();
- }
- var closeObserver = _this._config.closeObserver;
- if (closeObserver) {
- closeObserver.next(e);
- }
- if (e.wasClean) {
- observer.complete();
- }
- else {
- observer.error(e);
- }
- };
- socket.onmessage = function (e) {
- try {
- var deserializer = _this._config.deserializer;
- observer.next(deserializer(e));
- }
- catch (err) {
- observer.error(err);
- }
- };
- };
- WebSocketSubject.prototype._subscribe = function (subscriber) {
- var _this = this;
- var source = this.source;
- if (source) {
- return source.subscribe(subscriber);
- }
- if (!this._socket) {
- this._connectSocket();
- }
- this._output.subscribe(subscriber);
- subscriber.add(function () {
- var _socket = _this._socket;
- if (_this._output.observers.length === 0) {
- if (_socket && (_socket.readyState === 1 || _socket.readyState === 0)) {
- _socket.close();
- }
- _this._resetState();
- }
- });
- return subscriber;
- };
- WebSocketSubject.prototype.unsubscribe = function () {
- var _socket = this._socket;
- if (_socket && (_socket.readyState === 1 || _socket.readyState === 0)) {
- _socket.close();
- }
- this._resetState();
- _super.prototype.unsubscribe.call(this);
- };
- return WebSocketSubject;
-}(AnonymousSubject));
-export { WebSocketSubject };
-//# sourceMappingURL=WebSocketSubject.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm5/internal/observable/dom/WebSocketSubject.js.map b/node_modules/rxjs/dist/esm5/internal/observable/dom/WebSocketSubject.js.map
deleted file mode 100644
index 005a2c3..0000000
--- a/node_modules/rxjs/dist/esm5/internal/observable/dom/WebSocketSubject.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"WebSocketSubject.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/WebSocketSubject.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm5/internal/observable/dom/animationFrames.js b/node_modules/rxjs/dist/esm5/internal/observable/dom/animationFrames.js
deleted file mode 100644
index 8fec6cd..0000000
--- a/node_modules/rxjs/dist/esm5/internal/observable/dom/animationFrames.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Observable } from '../../Observable';
-import { performanceTimestampProvider } from '../../scheduler/performanceTimestampProvider';
-import { animationFrameProvider } from '../../scheduler/animationFrameProvider';
-export function animationFrames(timestampProvider) {
- return timestampProvider ? animationFramesFactory(timestampProvider) : DEFAULT_ANIMATION_FRAMES;
-}
-function animationFramesFactory(timestampProvider) {
- return new Observable(function (subscriber) {
- var provider = timestampProvider || performanceTimestampProvider;
- var start = provider.now();
- var id = 0;
- var run = function () {
- if (!subscriber.closed) {
- id = animationFrameProvider.requestAnimationFrame(function (timestamp) {
- id = 0;
- var now = provider.now();
- subscriber.next({
- timestamp: timestampProvider ? now : timestamp,
- elapsed: now - start,
- });
- run();
- });
- }
- };
- run();
- return function () {
- if (id) {
- animationFrameProvider.cancelAnimationFrame(id);
- }
- };
- });
-}
-var DEFAULT_ANIMATION_FRAMES = animationFramesFactory();
-//# sourceMappingURL=animationFrames.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm5/internal/observable/dom/animationFrames.js.map b/node_modules/rxjs/dist/esm5/internal/observable/dom/animationFrames.js.map
deleted file mode 100644
index e5af4e4..0000000
--- a/node_modules/rxjs/dist/esm5/internal/observable/dom/animationFrames.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"animationFrames.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/animationFrames.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,4BAA4B,EAAE,MAAM,8CAA8C,CAAC;AAC5F,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAuEhF,MAAM,UAAU,eAAe,CAAC,iBAAqC;IACnE,OAAO,iBAAiB,CAAC,CAAC,CAAC,sBAAsB,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC;AAClG,CAAC;AAMD,SAAS,sBAAsB,CAAC,iBAAqC;IACnE,OAAO,IAAI,UAAU,CAAyC,UAAC,UAAU;QAIvE,IAAM,QAAQ,GAAG,iBAAiB,IAAI,4BAA4B,CAAC;QAMnE,IAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;QAE7B,IAAI,EAAE,GAAG,CAAC,CAAC;QACX,IAAM,GAAG,GAAG;YACV,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE;gBACtB,EAAE,GAAG,sBAAsB,CAAC,qBAAqB,CAAC,UAAC,SAAuC;oBACxF,EAAE,GAAG,CAAC,CAAC;oBAQP,IAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;oBAC3B,UAAU,CAAC,IAAI,CAAC;wBACd,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;wBAC9C,OAAO,EAAE,GAAG,GAAG,KAAK;qBACrB,CAAC,CAAC;oBACH,GAAG,EAAE,CAAC;gBACR,CAAC,CAAC,CAAC;aACJ;QACH,CAAC,CAAC;QAEF,GAAG,EAAE,CAAC;QAEN,OAAO;YACL,IAAI,EAAE,EAAE;gBACN,sBAAsB,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC;aACjD;QACH,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAMD,IAAM,wBAAwB,GAAG,sBAAsB,EAAE,CAAC"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm5/internal/observable/dom/fetch.js b/node_modules/rxjs/dist/esm5/internal/observable/dom/fetch.js
deleted file mode 100644
index ff9361e..0000000
--- a/node_modules/rxjs/dist/esm5/internal/observable/dom/fetch.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { __assign, __rest } from "tslib";
-import { createOperatorSubscriber } from '../../operators/OperatorSubscriber';
-import { Observable } from '../../Observable';
-import { innerFrom } from '../../observable/innerFrom';
-export function fromFetch(input, initWithSelector) {
- if (initWithSelector === void 0) { initWithSelector = {}; }
- var selector = initWithSelector.selector, init = __rest(initWithSelector, ["selector"]);
- return new Observable(function (subscriber) {
- var controller = new AbortController();
- var signal = controller.signal;
- var abortable = true;
- var outerSignal = init.signal;
- if (outerSignal) {
- if (outerSignal.aborted) {
- controller.abort();
- }
- else {
- var outerSignalHandler_1 = function () {
- if (!signal.aborted) {
- controller.abort();
- }
- };
- outerSignal.addEventListener('abort', outerSignalHandler_1);
- subscriber.add(function () { return outerSignal.removeEventListener('abort', outerSignalHandler_1); });
- }
- }
- var perSubscriberInit = __assign(__assign({}, init), { signal: signal });
- var handleError = function (err) {
- abortable = false;
- subscriber.error(err);
- };
- fetch(input, perSubscriberInit)
- .then(function (response) {
- if (selector) {
- innerFrom(selector(response)).subscribe(createOperatorSubscriber(subscriber, undefined, function () {
- abortable = false;
- subscriber.complete();
- }, handleError));
- }
- else {
- abortable = false;
- subscriber.next(response);
- subscriber.complete();
- }
- })
- .catch(handleError);
- return function () {
- if (abortable) {
- controller.abort();
- }
- };
- });
-}
-//# sourceMappingURL=fetch.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm5/internal/observable/dom/fetch.js.map b/node_modules/rxjs/dist/esm5/internal/observable/dom/fetch.js.map
deleted file mode 100644
index c402c03..0000000
--- a/node_modules/rxjs/dist/esm5/internal/observable/dom/fetch.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/fetch.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AA4FvD,MAAM,UAAU,SAAS,CACvB,KAAuB,EACvB,gBAEM;IAFN,iCAAA,EAAA,qBAEM;IAEE,IAAA,QAAQ,GAAc,gBAAgB,SAA9B,EAAK,IAAI,UAAK,gBAAgB,EAAxC,YAAqB,CAAF,CAAsB;IAC/C,OAAO,IAAI,UAAU,CAAe,UAAC,UAAU;QAK7C,IAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACjC,IAAA,MAAM,GAAK,UAAU,OAAf,CAAgB;QAK9B,IAAI,SAAS,GAAG,IAAI,CAAC;QAKb,IAAQ,WAAW,GAAK,IAAI,OAAT,CAAU;QACrC,IAAI,WAAW,EAAE;YACf,IAAI,WAAW,CAAC,OAAO,EAAE;gBACvB,UAAU,CAAC,KAAK,EAAE,CAAC;aACpB;iBAAM;gBAGL,IAAM,oBAAkB,GAAG;oBACzB,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE;wBACnB,UAAU,CAAC,KAAK,EAAE,CAAC;qBACpB;gBACH,CAAC,CAAC;gBACF,WAAW,CAAC,gBAAgB,CAAC,OAAO,EAAE,oBAAkB,CAAC,CAAC;gBAC1D,UAAU,CAAC,GAAG,CAAC,cAAM,OAAA,WAAW,CAAC,mBAAmB,CAAC,OAAO,EAAE,oBAAkB,CAAC,EAA5D,CAA4D,CAAC,CAAC;aACpF;SACF;QAOD,IAAM,iBAAiB,yBAAqB,IAAI,KAAE,MAAM,QAAA,GAAE,CAAC;QAE3D,IAAM,WAAW,GAAG,UAAC,GAAQ;YAC3B,SAAS,GAAG,KAAK,CAAC;YAClB,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC,CAAC;QAEF,KAAK,CAAC,KAAK,EAAE,iBAAiB,CAAC;aAC5B,IAAI,CAAC,UAAC,QAAQ;YACb,IAAI,QAAQ,EAAE;gBAIZ,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CACrC,wBAAwB,CACtB,UAAU,EAEV,SAAS,EAET;oBACE,SAAS,GAAG,KAAK,CAAC;oBAClB,UAAU,CAAC,QAAQ,EAAE,CAAC;gBACxB,CAAC,EACD,WAAW,CACZ,CACF,CAAC;aACH;iBAAM;gBACL,SAAS,GAAG,KAAK,CAAC;gBAClB,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBAC1B,UAAU,CAAC,QAAQ,EAAE,CAAC;aACvB;QACH,CAAC,CAAC;aACD,KAAK,CAAC,WAAW,CAAC,CAAC;QAEtB,OAAO;YACL,IAAI,SAAS,EAAE;gBACb,UAAU,CAAC,KAAK,EAAE,CAAC;aACpB;QACH,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm5/internal/observable/dom/webSocket.js b/node_modules/rxjs/dist/esm5/internal/observable/dom/webSocket.js
deleted file mode 100644
index 73a51ab..0000000
--- a/node_modules/rxjs/dist/esm5/internal/observable/dom/webSocket.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { WebSocketSubject } from './WebSocketSubject';
-export function webSocket(urlConfigOrSource) {
- return new WebSocketSubject(urlConfigOrSource);
-}
-//# sourceMappingURL=webSocket.js.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/esm5/internal/observable/dom/webSocket.js.map b/node_modules/rxjs/dist/esm5/internal/observable/dom/webSocket.js.map
deleted file mode 100644
index f284743..0000000
--- a/node_modules/rxjs/dist/esm5/internal/observable/dom/webSocket.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"webSocket.js","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/webSocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAA0B,MAAM,oBAAoB,CAAC;AA8J9E,MAAM,UAAU,SAAS,CAAI,iBAAqD;IAChF,OAAO,IAAI,gBAAgB,CAAI,iBAAiB,CAAC,CAAC;AACpD,CAAC"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/types/internal/observable/dom/WebSocketSubject.d.ts b/node_modules/rxjs/dist/types/internal/observable/dom/WebSocketSubject.d.ts
deleted file mode 100644
index d0f48c2..0000000
--- a/node_modules/rxjs/dist/types/internal/observable/dom/WebSocketSubject.d.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import { AnonymousSubject } from '../../Subject';
-import { Observable } from '../../Observable';
-import { Operator } from '../../Operator';
-import { Observer, NextObserver } from '../../types';
-/**
- * WebSocketSubjectConfig is a plain Object that allows us to make our
- * webSocket configurable.
- *
- * Provides flexibility to {@link webSocket}
- *
- * It defines a set of properties to provide custom behavior in specific
- * moments of the socket's lifecycle. When the connection opens we can
- * use `openObserver`, when the connection is closed `closeObserver`, if we
- * are interested in listening for data coming from server: `deserializer`,
- * which allows us to customize the deserialization strategy of data before passing it
- * to the socket client. By default, `deserializer` is going to apply `JSON.parse` to each message coming
- * from the Server.
- *
- * ## Examples
- *
- * **deserializer**, the default for this property is `JSON.parse` but since there are just two options
- * for incoming data, either be text or binary data. We can apply a custom deserialization strategy
- * or just simply skip the default behaviour.
- *
- * ```ts
- * import { webSocket } from 'rxjs/webSocket';
- *
- * const wsSubject = webSocket({
- * url: 'ws://localhost:8081',
- * //Apply any transformation of your choice.
- * deserializer: ({ data }) => data
- * });
- *
- * wsSubject.subscribe(console.log);
- *
- * // Let's suppose we have this on the Server: ws.send('This is a msg from the server')
- * //output
- * //
- * // This is a msg from the server
- * ```
- *
- * **serializer** allows us to apply custom serialization strategy but for the outgoing messages.
- *
- * ```ts
- * import { webSocket } from 'rxjs/webSocket';
- *
- * const wsSubject = webSocket({
- * url: 'ws://localhost:8081',
- * // Apply any transformation of your choice.
- * serializer: msg => JSON.stringify({ channel: 'webDevelopment', msg: msg })
- * });
- *
- * wsSubject.subscribe(() => subject.next('msg to the server'));
- *
- * // Let's suppose we have this on the Server:
- * // ws.on('message', msg => console.log);
- * // ws.send('This is a msg from the server');
- * // output at server side:
- * //
- * // {"channel":"webDevelopment","msg":"msg to the server"}
- * ```
- *
- * **closeObserver** allows us to set a custom error when an error raises up.
- *
- * ```ts
- * import { webSocket } from 'rxjs/webSocket';
- *
- * const wsSubject = webSocket({
- * url: 'ws://localhost:8081',
- * closeObserver: {
- * next() {
- * const customError = { code: 6666, reason: 'Custom evil reason' }
- * console.log(`code: ${ customError.code }, reason: ${ customError.reason }`);
- * }
- * }
- * });
- *
- * // output
- * // code: 6666, reason: Custom evil reason
- * ```
- *
- * **openObserver**, Let's say we need to make some kind of init task before sending/receiving msgs to the
- * webSocket or sending notification that the connection was successful, this is when
- * openObserver is useful for.
- *
- * ```ts
- * import { webSocket } from 'rxjs/webSocket';
- *
- * const wsSubject = webSocket({
- * url: 'ws://localhost:8081',
- * openObserver: {
- * next: () => {
- * console.log('Connection ok');
- * }
- * }
- * });
- *
- * // output
- * // Connection ok
- * ```
- */
-export interface WebSocketSubjectConfig {
- /** The url of the socket server to connect to */
- url: string;
- /** The protocol to use to connect */
- protocol?: string | Array;
- /** @deprecated Will be removed in v8. Use {@link deserializer} instead. */
- resultSelector?: (e: MessageEvent) => T;
- /**
- * A serializer used to create messages from passed values before the
- * messages are sent to the server. Defaults to JSON.stringify.
- */
- serializer?: (value: T) => WebSocketMessage;
- /**
- * A deserializer used for messages arriving on the socket from the
- * server. Defaults to JSON.parse.
- */
- deserializer?: (e: MessageEvent) => T;
- /**
- * An Observer that watches when open events occur on the underlying web socket.
- */
- openObserver?: NextObserver;
- /**
- * An Observer that watches when close events occur on the underlying web socket
- */
- closeObserver?: NextObserver;
- /**
- * An Observer that watches when a close is about to occur due to
- * unsubscription.
- */
- closingObserver?: NextObserver;
- /**
- * A WebSocket constructor to use. This is useful for situations like using a
- * WebSocket impl in Node (WebSocket is a DOM API), or for mocking a WebSocket
- * for testing purposes
- */
- WebSocketCtor?: {
- new (url: string, protocols?: string | string[]): WebSocket;
- };
- /** Sets the `binaryType` property of the underlying WebSocket. */
- binaryType?: 'blob' | 'arraybuffer';
-}
-export declare type WebSocketMessage = string | ArrayBuffer | Blob | ArrayBufferView;
-export declare class WebSocketSubject extends AnonymousSubject {
- private _config;
- private _socket;
- constructor(urlConfigOrSource: string | WebSocketSubjectConfig | Observable, destination?: Observer);
- /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */
- lift(operator: Operator): WebSocketSubject;
- private _resetState;
- /**
- * Creates an {@link Observable}, that when subscribed to, sends a message,
- * defined by the `subMsg` function, to the server over the socket to begin a
- * subscription to data over that socket. Once data arrives, the
- * `messageFilter` argument will be used to select the appropriate data for
- * the resulting Observable. When finalization occurs, either due to
- * unsubscription, completion, or error, a message defined by the `unsubMsg`
- * argument will be sent to the server over the WebSocketSubject.
- *
- * @param subMsg A function to generate the subscription message to be sent to
- * the server. This will still be processed by the serializer in the
- * WebSocketSubject's config. (Which defaults to JSON serialization)
- * @param unsubMsg A function to generate the unsubscription message to be
- * sent to the server at finalization. This will still be processed by the
- * serializer in the WebSocketSubject's config.
- * @param messageFilter A predicate for selecting the appropriate messages
- * from the server for the output stream.
- */
- multiplex(subMsg: () => any, unsubMsg: () => any, messageFilter: (value: T) => boolean): Observable;
- private _connectSocket;
- unsubscribe(): void;
-}
-//# sourceMappingURL=WebSocketSubject.d.ts.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/types/internal/observable/dom/WebSocketSubject.d.ts.map b/node_modules/rxjs/dist/types/internal/observable/dom/WebSocketSubject.d.ts.map
deleted file mode 100644
index 173c4d4..0000000
--- a/node_modules/rxjs/dist/types/internal/observable/dom/WebSocketSubject.d.ts.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"WebSocketSubject.d.ts","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/WebSocketSubject.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgGG;AACH,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAClC,2EAA2E;IAC3E,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,CAAC,CAAC;IACxC;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,gBAAgB,CAAC;IAC5C;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,CAAC,CAAC;IACtC;;OAEG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IACnC;;OAEG;IACH,aAAa,CAAC,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;IACzC;;;OAGG;IACH,eAAe,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IACrC;;;;OAIG;IACH,aAAa,CAAC,EAAE;QAAE,KAAK,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAC;IAChF,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC;CACrC;AAWD,oBAAY,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,IAAI,GAAG,eAAe,CAAC;AAE7E,qBAAa,gBAAgB,CAAC,CAAC,CAAE,SAAQ,gBAAgB,CAAC,CAAC,CAAC;IAE1D,OAAO,CAAC,OAAO,CAA4B;IAM3C,OAAO,CAAC,OAAO,CAA0B;gBAE7B,iBAAiB,EAAE,MAAM,GAAG,sBAAsB,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;IA2B5G,oGAAoG;IACpG,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC;IAOtD,OAAO,CAAC,WAAW;IAQnB;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,EAAE,aAAa,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO;IAkCtF,OAAO,CAAC,cAAc;IA+HtB,WAAW;CAQZ"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/types/internal/observable/dom/animationFrames.d.ts b/node_modules/rxjs/dist/types/internal/observable/dom/animationFrames.d.ts
deleted file mode 100644
index 2f7659f..0000000
--- a/node_modules/rxjs/dist/types/internal/observable/dom/animationFrames.d.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Observable } from '../../Observable';
-import { TimestampProvider } from '../../types';
-/**
- * An observable of animation frames
- *
- * Emits the amount of time elapsed since subscription and the timestamp on each animation frame.
- * Defaults to milliseconds provided to the requestAnimationFrame's callback. Does not end on its own.
- *
- * Every subscription will start a separate animation loop. Since animation frames are always scheduled
- * by the browser to occur directly before a repaint, scheduling more than one animation frame synchronously
- * should not be much different or have more overhead than looping over an array of events during
- * a single animation frame. However, if for some reason the developer would like to ensure the
- * execution of animation-related handlers are all executed during the same task by the engine,
- * the `share` operator can be used.
- *
- * This is useful for setting up animations with RxJS.
- *
- * ## Examples
- *
- * Tweening a div to move it on the screen
- *
- * ```ts
- * import { animationFrames, map, takeWhile, endWith } from 'rxjs';
- *
- * function tween(start: number, end: number, duration: number) {
- * const diff = end - start;
- * return animationFrames().pipe(
- * // Figure out what percentage of time has passed
- * map(({ elapsed }) => elapsed / duration),
- * // Take the vector while less than 100%
- * takeWhile(v => v < 1),
- * // Finish with 100%
- * endWith(1),
- * // Calculate the distance traveled between start and end
- * map(v => v * diff + start)
- * );
- * }
- *
- * // Setup a div for us to move around
- * const div = document.createElement('div');
- * document.body.appendChild(div);
- * div.style.position = 'absolute';
- * div.style.width = '40px';
- * div.style.height = '40px';
- * div.style.backgroundColor = 'lime';
- * div.style.transform = 'translate3d(10px, 0, 0)';
- *
- * tween(10, 200, 4000).subscribe(x => {
- * div.style.transform = `translate3d(${ x }px, 0, 0)`;
- * });
- * ```
- *
- * Providing a custom timestamp provider
- *
- * ```ts
- * import { animationFrames, TimestampProvider } from 'rxjs';
- *
- * // A custom timestamp provider
- * let now = 0;
- * const customTSProvider: TimestampProvider = {
- * now() { return now++; }
- * };
- *
- * const source$ = animationFrames(customTSProvider);
- *
- * // Log increasing numbers 0...1...2... on every animation frame.
- * source$.subscribe(({ elapsed }) => console.log(elapsed));
- * ```
- *
- * @param timestampProvider An object with a `now` method that provides a numeric timestamp
- */
-export declare function animationFrames(timestampProvider?: TimestampProvider): Observable<{
- timestamp: number;
- elapsed: number;
-}>;
-//# sourceMappingURL=animationFrames.d.ts.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/types/internal/observable/dom/animationFrames.d.ts.map b/node_modules/rxjs/dist/types/internal/observable/dom/animationFrames.d.ts.map
deleted file mode 100644
index cf0f7a2..0000000
--- a/node_modules/rxjs/dist/types/internal/observable/dom/animationFrames.d.ts.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"animationFrames.d.ts","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/animationFrames.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AACH,wBAAgB,eAAe,CAAC,iBAAiB,CAAC,EAAE,iBAAiB;;;GAEpE"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/types/internal/observable/dom/fetch.d.ts b/node_modules/rxjs/dist/types/internal/observable/dom/fetch.d.ts
deleted file mode 100644
index f2ded40..0000000
--- a/node_modules/rxjs/dist/types/internal/observable/dom/fetch.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Observable } from '../../Observable';
-import { ObservableInput } from '../../types';
-export declare function fromFetch(input: string | Request, init: RequestInit & {
- selector: (response: Response) => ObservableInput;
-}): Observable;
-export declare function fromFetch(input: string | Request, init?: RequestInit): Observable;
-//# sourceMappingURL=fetch.d.ts.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/types/internal/observable/dom/fetch.d.ts.map b/node_modules/rxjs/dist/types/internal/observable/dom/fetch.d.ts.map
deleted file mode 100644
index 19a860b..0000000
--- a/node_modules/rxjs/dist/types/internal/observable/dom/fetch.d.ts.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/fetch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,wBAAgB,SAAS,CAAC,CAAC,EACzB,KAAK,EAAE,MAAM,GAAG,OAAO,EACvB,IAAI,EAAE,WAAW,GAAG;IAClB,QAAQ,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,eAAe,CAAC,CAAC,CAAC,CAAC;CACtD,GACA,UAAU,CAAC,CAAC,CAAC,CAAC;AAEjB,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC"}
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/types/internal/observable/dom/webSocket.d.ts b/node_modules/rxjs/dist/types/internal/observable/dom/webSocket.d.ts
deleted file mode 100644
index 9dbb47b..0000000
--- a/node_modules/rxjs/dist/types/internal/observable/dom/webSocket.d.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { WebSocketSubject, WebSocketSubjectConfig } from './WebSocketSubject';
-/**
- * Wrapper around the w3c-compatible WebSocket object provided by the browser.
- *
- * {@link Subject} that communicates with a server via WebSocket
- *
- * `webSocket` is a factory function that produces a `WebSocketSubject`,
- * which can be used to make WebSocket connection with an arbitrary endpoint.
- * `webSocket` accepts as an argument either a string with url of WebSocket endpoint, or an
- * {@link WebSocketSubjectConfig} object for providing additional configuration, as
- * well as Observers for tracking lifecycle of WebSocket connection.
- *
- * When `WebSocketSubject` is subscribed, it attempts to make a socket connection,
- * unless there is one made already. This means that many subscribers will always listen
- * on the same socket, thus saving resources. If however, two instances are made of `WebSocketSubject`,
- * even if these two were provided with the same url, they will attempt to make separate
- * connections. When consumer of a `WebSocketSubject` unsubscribes, socket connection is closed,
- * only if there are no more subscribers still listening. If after some time a consumer starts
- * subscribing again, connection is reestablished.
- *
- * Once connection is made, whenever a new message comes from the server, `WebSocketSubject` will emit that
- * message as a value in the stream. By default, a message from the socket is parsed via `JSON.parse`. If you
- * want to customize how deserialization is handled (if at all), you can provide custom `resultSelector`
- * function in {@link WebSocketSubject}. When connection closes, stream will complete, provided it happened without
- * any errors. If at any point (starting, maintaining or closing a connection) there is an error,
- * stream will also error with whatever WebSocket API has thrown.
- *
- * By virtue of being a {@link Subject}, `WebSocketSubject` allows for receiving and sending messages from the server. In order
- * to communicate with a connected endpoint, use `next`, `error` and `complete` methods. `next` sends a value to the server, so bear in mind
- * that this value will not be serialized beforehand. Because of This, `JSON.stringify` will have to be called on a value by hand,
- * before calling `next` with a result. Note also that if at the moment of nexting value
- * there is no socket connection (for example no one is subscribing), those values will be buffered, and sent when connection
- * is finally established. `complete` method closes socket connection. `error` does the same,
- * as well as notifying the server that something went wrong via status code and string with details of what happened.
- * Since status code is required in WebSocket API, `WebSocketSubject` does not allow, like regular `Subject`,
- * arbitrary values being passed to the `error` method. It needs to be called with an object that has `code`
- * property with status code number and optional `reason` property with string describing details
- * of an error.
- *
- * Calling `next` does not affect subscribers of `WebSocketSubject` - they have no
- * information that something was sent to the server (unless of course the server
- * responds somehow to a message). On the other hand, since calling `complete` triggers
- * an attempt to close socket connection. If that connection is closed without any errors, stream will
- * complete, thus notifying all subscribers. And since calling `error` closes
- * socket connection as well, just with a different status code for the server, if closing itself proceeds
- * without errors, subscribed Observable will not error, as one might expect, but complete as usual. In both cases
- * (calling `complete` or `error`), if process of closing socket connection results in some errors, *then* stream
- * will error.
- *
- * **Multiplexing**
- *
- * `WebSocketSubject` has an additional operator, not found in other Subjects. It is called `multiplex` and it is
- * used to simulate opening several socket connections, while in reality maintaining only one.
- * For example, an application has both chat panel and real-time notifications about sport news. Since these are two distinct functions,
- * it would make sense to have two separate connections for each. Perhaps there could even be two separate services with WebSocket
- * endpoints, running on separate machines with only GUI combining them together. Having a socket connection
- * for each functionality could become too resource expensive. It is a common pattern to have single
- * WebSocket endpoint that acts as a gateway for the other services (in this case chat and sport news services).
- * Even though there is a single connection in a client app, having the ability to manipulate streams as if it
- * were two separate sockets is desirable. This eliminates manually registering and unregistering in a gateway for
- * given service and filter out messages of interest. This is exactly what `multiplex` method is for.
- *
- * Method accepts three parameters. First two are functions returning subscription and unsubscription messages
- * respectively. These are messages that will be sent to the server, whenever consumer of resulting Observable
- * subscribes and unsubscribes. Server can use them to verify that some kind of messages should start or stop
- * being forwarded to the client. In case of the above example application, after getting subscription message with proper identifier,
- * gateway server can decide that it should connect to real sport news service and start forwarding messages from it.
- * Note that both messages will be sent as returned by the functions, they are by default serialized using JSON.stringify, just
- * as messages pushed via `next`. Also bear in mind that these messages will be sent on *every* subscription and
- * unsubscription. This is potentially dangerous, because one consumer of an Observable may unsubscribe and the server
- * might stop sending messages, since it got unsubscription message. This needs to be handled
- * on the server or using {@link publish} on a Observable returned from 'multiplex'.
- *
- * Last argument to `multiplex` is a `messageFilter` function which should return a boolean. It is used to filter out messages
- * sent by the server to only those that belong to simulated WebSocket stream. For example, server might mark these
- * messages with some kind of string identifier on a message object and `messageFilter` would return `true`
- * if there is such identifier on an object emitted by the socket. Messages which returns `false` in `messageFilter` are simply skipped,
- * and are not passed down the stream.
- *
- * Return value of `multiplex` is an Observable with messages incoming from emulated socket connection. Note that this
- * is not a `WebSocketSubject`, so calling `next` or `multiplex` again will fail. For pushing values to the
- * server, use root `WebSocketSubject`.
- *
- * ## Examples
- *
- * Listening for messages from the server
- *
- * ```ts
- * import { webSocket } from 'rxjs/webSocket';
- *
- * const subject = webSocket('ws://localhost:8081');
- *
- * subject.subscribe({
- * next: msg => console.log('message received: ' + msg), // Called whenever there is a message from the server.
- * error: err => console.log(err), // Called if at any point WebSocket API signals some kind of error.
- * complete: () => console.log('complete') // Called when connection is closed (for whatever reason).
- * });
- * ```
- *
- * Pushing messages to the server
- *
- * ```ts
- * import { webSocket } from 'rxjs/webSocket';
- *
- * const subject = webSocket('ws://localhost:8081');
- *
- * subject.subscribe();
- * // Note that at least one consumer has to subscribe to the created subject - otherwise "nexted" values will be just buffered and not sent,
- * // since no connection was established!
- *
- * subject.next({ message: 'some message' });
- * // This will send a message to the server once a connection is made. Remember value is serialized with JSON.stringify by default!
- *
- * subject.complete(); // Closes the connection.
- *
- * subject.error({ code: 4000, reason: 'I think our app just broke!' });
- * // Also closes the connection, but let's the server know that this closing is caused by some error.
- * ```
- *
- * Multiplexing WebSocket
- *
- * ```ts
- * import { webSocket } from 'rxjs/webSocket';
- *
- * const subject = webSocket('ws://localhost:8081');
- *
- * const observableA = subject.multiplex(
- * () => ({ subscribe: 'A' }), // When server gets this message, it will start sending messages for 'A'...
- * () => ({ unsubscribe: 'A' }), // ...and when gets this one, it will stop.
- * message => message.type === 'A' // If the function returns `true` message is passed down the stream. Skipped if the function returns false.
- * );
- *
- * const observableB = subject.multiplex( // And the same goes for 'B'.
- * () => ({ subscribe: 'B' }),
- * () => ({ unsubscribe: 'B' }),
- * message => message.type === 'B'
- * );
- *
- * const subA = observableA.subscribe(messageForA => console.log(messageForA));
- * // At this moment WebSocket connection is established. Server gets '{"subscribe": "A"}' message and starts sending messages for 'A',
- * // which we log here.
- *
- * const subB = observableB.subscribe(messageForB => console.log(messageForB));
- * // Since we already have a connection, we just send '{"subscribe": "B"}' message to the server. It starts sending messages for 'B',
- * // which we log here.
- *
- * subB.unsubscribe();
- * // Message '{"unsubscribe": "B"}' is sent to the server, which stops sending 'B' messages.
- *
- * subA.unsubscribe();
- * // Message '{"unsubscribe": "A"}' makes the server stop sending messages for 'A'. Since there is no more subscribers to root Subject,
- * // socket connection closes.
- * ```
- *
- * @param urlConfigOrSource The WebSocket endpoint as an url or an object with configuration and additional Observers.
- * @return Subject which allows to both send and receive messages via WebSocket connection.
- */
-export declare function webSocket(urlConfigOrSource: string | WebSocketSubjectConfig): WebSocketSubject;
-//# sourceMappingURL=webSocket.d.ts.map
\ No newline at end of file
diff --git a/node_modules/rxjs/dist/types/internal/observable/dom/webSocket.d.ts.map b/node_modules/rxjs/dist/types/internal/observable/dom/webSocket.d.ts.map
deleted file mode 100644
index ec0cc8e..0000000
--- a/node_modules/rxjs/dist/types/internal/observable/dom/webSocket.d.ts.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"webSocket.d.ts","sourceRoot":"","sources":["../../../../../src/internal/observable/dom/webSocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2JG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,iBAAiB,EAAE,MAAM,GAAG,sBAAsB,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAEvG"}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..e203331
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,833 @@
+{
+ "name": "dscode-v2",
+ "version": "2.0.1-0712-NEXT",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "dscode-v2",
+ "version": "2.0.1-0712-NEXT",
+ "dependencies": {
+ "electron": "33.4.11"
+ },
+ "devDependencies": {
+ "playwright": "1.64.0"
+ }
+ },
+ "node_modules/@electron/get": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
+ "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "env-paths": "^2.2.0",
+ "fs-extra": "^8.1.0",
+ "got": "^11.8.5",
+ "progress": "^2.0.3",
+ "semver": "^6.2.0",
+ "sumchecker": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "global-agent": "^3.0.0"
+ }
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@szmarczak/http-timer": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
+ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
+ "license": "MIT",
+ "dependencies": {
+ "defer-to-connect": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@types/cacheable-request": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
+ "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-cache-semantics": "*",
+ "@types/keyv": "^3.1.4",
+ "@types/node": "*",
+ "@types/responselike": "^1.0.0"
+ }
+ },
+ "node_modules/@types/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/keyv": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
+ "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.43",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
+ "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/responselike": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
+ "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/boolean": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/cacheable-lookup": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+ "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.6.0"
+ }
+ },
+ "node_modules/cacheable-request": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
+ "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
+ "license": "MIT",
+ "dependencies": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^4.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^6.0.1",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/clone-response": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
+ "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decompress-response/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defer-to-connect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/electron": {
+ "version": "33.4.11",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz",
+ "integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@electron/get": "^2.0.0",
+ "@types/node": "^20.9.0",
+ "extract-zip": "^2.0.1"
+ },
+ "bin": {
+ "electron": "cli.js"
+ },
+ "engines": {
+ "node": ">= 12.20.55"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es6-error": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/global-agent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
+ "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "es6-error": "^4.1.1",
+ "matcher": "^3.0.0",
+ "roarr": "^2.15.3",
+ "semver": "^7.3.2",
+ "serialize-error": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=10.0"
+ }
+ },
+ "node_modules/global-agent/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/got": {
+ "version": "11.8.6",
+ "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
+ "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/is": "^4.0.0",
+ "@szmarczak/http-timer": "^4.0.5",
+ "@types/cacheable-request": "^6.0.1",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^5.0.3",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "http2-wrapper": "^1.0.0-beta.5.2",
+ "lowercase-keys": "^2.0.0",
+ "p-cancelable": "^2.0.0",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz",
+ "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/http2-wrapper": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
+ "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+ "license": "MIT",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "license": "MIT"
+ },
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "license": "MIT",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/matcher": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
+ "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "escape-string-regexp": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/normalize-url": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/p-cancelable": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
+ "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "license": "MIT"
+ },
+ "node_modules/playwright": {
+ "version": "1.64.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.0.tgz",
+ "integrity": "sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.64.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.64.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0.tgz",
+ "integrity": "sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/resolve-alpn": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "license": "MIT"
+ },
+ "node_modules/responselike": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
+ "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
+ "license": "MIT",
+ "dependencies": {
+ "lowercase-keys": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/roarr": {
+ "version": "2.15.4",
+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
+ "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "detect-node": "^2.0.4",
+ "globalthis": "^1.0.1",
+ "json-stringify-safe": "^5.0.1",
+ "semver-compare": "^1.0.0",
+ "sprintf-js": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/serialize-error": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "type-fest": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/sumchecker": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
+ "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
+ "license": "(MIT OR CC0-1.0)",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index acca814..d571b19 100644
--- a/package.json
+++ b/package.json
@@ -6,12 +6,13 @@
"scripts": {
"start": "electron .",
"sidecar": "python3 sidecar/sidecar.py",
- "dev": "concurrently \"npm run sidecar\" \"npm start\""
+ "dev": "electron . --dev",
+ "test": "node --test tests/*.test.js"
},
"dependencies": {
- "electron": "^33.0.0"
+ "electron": "33.4.11"
},
"devDependencies": {
- "concurrently": "^9.0.0"
+ "playwright": "1.64.0"
}
}
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..158a118
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,12 @@
+[tool.ruff.lint]
+select = ["E9", "F821", "F822", "F823", "F601", "F811", "B006", "B012", "B023"]
+
+[tool.coverage.run]
+source = ["sidecar", "src"]
+patch = ["subprocess"]
+parallel = true
+sigterm = true
+omit = ["engine/src/gui/*"]
+
+[tool.coverage.report]
+skip_empty = true
diff --git a/renderer/css/app.css b/renderer/css/app.css
index 9076e66..e34db15 100644
--- a/renderer/css/app.css
+++ b/renderer/css/app.css
@@ -342,3 +342,26 @@
}
.code-block { background: var(--code-bg); padding: 10px 14px; border-radius: 6px; font-family: var(--font-code); font-size: 12px; line-height: 1.5; overflow-x: auto; margin: 6px 0; white-space: pre; }
.inline-code { background: var(--code-bg); padding: 1px 4px; border-radius: 3px; font-family: var(--font-code); font-size: 12px; color: var(--accent); }
+/* Engine confirmations and text entry dialogs. */
+.dialog-overlay { position:fixed; inset:0; z-index:1000; background:rgba(0,0,0,.35); display:grid; place-items:center; }
+.dialog-panel { background:var(--card); color:var(--text-2); padding:24px; border-radius:16px; width:min(560px,85vw); max-height:80vh; overflow:auto; }
+.dialog-panel pre { white-space:pre-wrap; overflow-wrap:anywhere; font:inherit; }
+.dialog-panel label { display:block; margin:12px 0; }
+.dialog-panel textarea { display:block; width:100%; min-height:64px; margin-top:8px; }
+.dialog-buttons { display:flex; justify-content:flex-end; gap:12px; margin-top:20px; }
+.dialog-buttons button { padding:8px 18px; cursor:pointer; }
+
+#attachment-list { display:flex; flex-wrap:wrap; gap:4px; max-width:35%; }
+#attachment-list:empty { display:none; }
+#attachment-list button,.project-file { border:1px solid var(--border); border-radius:6px; padding:5px 8px; background:var(--surface); color:var(--text); font-size:12px; cursor:pointer; }
+.project-file { display:block; width:100%; text-align:left; margin-bottom:5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+.dialog-panel textarea[readonly] { min-height:240px; font-family:monospace; }
+
+.tool-dot.failed { background:#d14343; }
+.tool-dot.finished { background:var(--text-3); }
+
+.ds-advanced { font-size:12px; margin-top:12px; color:var(--text); }
+.ds-advanced label { display:flex; justify-content:space-between; align-items:center; gap:8px; margin:8px 0; }
+.ds-advanced input,.ds-advanced select { max-width:160px; }
+.provider-row { display:flex; gap:6px; align-items:center; margin:8px 0; font-size:12px; }
+.provider-row span { flex:1; overflow:hidden; text-overflow:ellipsis; }
diff --git a/renderer/index.html b/renderer/index.html
index 9dfa501..76753fe 100644
--- a/renderer/index.html
+++ b/renderer/index.html
@@ -56,7 +56,9 @@ DS Code