Merge branch 'ji' into master
合并 ji 的功能:开放问答接口(/api/open/chat[/stream] 回传生成文件 + 临时不留存)、 技能地址管理、智能体详情「最近对话」等。 冲突 frontend StrategyRoutes.tsx 已解决:同时保留 admin/roundtable-diagnostics(master) 与 admin/skill-addresses(ji) 两个管理员路由。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
fbec222eb4
38 files changed
+3365
-94
No files matched your search
@@ -243,6 +243,7 @@ FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWA
|
||||
| **Thread Runs** (`/api/threads/{id}/runs`) | `POST /` - create background run; `POST /stream` - create + SSE stream; `POST /wait` - create + block; `GET /` - list runs; `GET /{rid}` - run details; `POST /{rid}/cancel` - cancel; `GET /{rid}/join` - join SSE; `GET /{rid}/messages` - paginated messages `{data, has_more}`; `GET /{rid}/events` - full event stream; `GET /../messages` - thread messages with feedback; `GET /../token-usage` - aggregate tokens |
|
||||
| **Feedback** (`/api/threads/{id}/runs/{rid}/feedback`) | `PUT /` - upsert feedback; `DELETE /` - delete user feedback; `POST /` - create feedback; `GET /` - list feedback; `GET /stats` - aggregate stats; `DELETE /{fid}` - delete specific |
|
||||
| **Runs** (`/api/runs`) | `POST /stream` - stateless run + SSE; `POST /wait` - stateless run + block; `GET /{rid}/messages` - paginated messages by run_id `{data, has_more}` (cursor: `after_seq`/`before_seq`); `GET /{rid}/feedback` - list feedback by run_id |
|
||||
| **Open Chat** (`/api/open/chat`) | **Unauthenticated** open Q&A entry for external systems (the `/api/open/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES` + CSRF-exempt in `csrf_middleware.should_check_csrf`). `POST /chat` `{message \| messages[], model_name?, agent_name?, agent_id?, thinking_enabled=false, strip_think=true, thread_id?, recursion_limit=100}` → resolves the agent by **Chinese display name** (`agent_name`, matched against each `.deer-flow/agents/*/config.yaml` `name` via `_resolve_agent_id`; explicit `agent_id` wins if given), builds the lead agent via `make_lead_agent` (so the chosen **agent's** SOUL/tools/skills all apply), `ainvoke`s it once (stateless — no checkpointer, no thread persistence; runs under the `"default"` user bucket), and returns **only the final** `{answer, model_name, agent_name, agent_id, thinking_enabled, thread_id}` (no intermediate tool-call/process). **`POST /chat/stream`** is the **SSE streaming variant** (same request body) for **long tasks** — the blocking `/chat` produces no downstream bytes for the whole run, so a front nginx `proxy_read_timeout` (default 60s) cuts it off with a **504**; the stream keeps the connection warm (a `: keepalive` SSE comment every `_HEARTBEAT_SECONDS=15`s via a background producer-task + queue, plus `event: delta` AI-text increments and `event: progress`), and ends with **`event: result`** carrying the same final `{answer, …}` JSON (or `event: error`). Clients only need to read `event: result`; intermediate events are ignorable. Both endpoints share `_build_run_context` (config/agent/thread). `GET /agents` lists `{name, id}` so callers know what `agent_name` to send. **Thinking off is reliable**: when `thinking_enabled=false` the route also sets `thinking_force_disabled=true` so the lead agent calls `create_chat_model(force_disable_thinking=True)`, which injects **three** OpenAI-compatible "thinking off" shapes into `extra_body` — top-level `enable_thinking=false` (**SiliconFlow / DashScope hybrid models** like `deepseek-ai/DeepSeek-V4-*` on `api.siliconflow.cn` — the knob the nested `chat_template_kwargs` shape does NOT reach), `chat_template_kwargs.enable_thinking=false` (vLLM/Qwen native), and `thinking.type="disabled"` (Zhipu GLM) — even for models that only declare `supports_thinking: true`. For the **web-UI path** (which sends `thinking_enabled=false` but NOT `force_disable`), SiliconFlow models need `when_thinking_disabled: {extra_body: {enable_thinking: false}}` in their `config.yaml` entry (added to `deepseek-ai/DeepSeek-V4-Flash`) for thinking-off to take effect. Sets `is_scheduled_run=true` internally to skip the auto-title LLM call. **Generated-file return (`include_files=true`, default on)**: an agent that produces its report as a file (writes to `/mnt/user-data/outputs/` + `present_files`) returns only a short summary in `answer`; to recover the real deliverable both endpoints now also return `files: [{name, virtual_path, encoding(text|base64), content}]` — `_collect_output_files` scans the run's `state['thread_data']['outputs_path']` (the open thread is stateless so artifacts can't be fetched by thread, but the endpoint runs server-side and the sandbox already wrote the files to disk), text decoded as utf-8 else base64, capped by `_MAX_OUTPUT_FILES=20` / `_MAX_OUTPUT_FILE_BYTES=5MB`. `/chat` puts `files` on the response; `/chat/stream` puts it in the `event: result` payload. A run with no answer **and** no files now 502s ("…文本答案或文件"). Router `app/gateway/routers/open_chat.py`; standalone caller `test_open_chat_api.py` (repo root, streaming by default); tests `tests/test_open_chat.py`. |
|
||||
| **Thread Shares** (`/api/thread-shares`) | Per-conversation sharing with two modes (`thread_shares.mode`). **`import`**: `POST /` `{thread_id, mode}` - create (or reuse) a revocable share code; `GET /{code}/preview` + `POST /{code}/import` - copy that conversation into the caller's account (fresh `thread_id` via `app/gateway/thread_copy.py` + new `threads_meta` row + `user-data` copy; dedup on `metadata.shared_origin`). **`view`**: a public, read-only static page ? see Public Shares below. Both modes: `GET /` - list my codes; `DELETE /{code}` - revoke. |
|
||||
| **Public Shares** (`/api/public/shares`) | **Unauthenticated** (the `/api/public/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES`). `GET /{code}` - read-only conversation snapshot (title + serialized messages + artifacts) for a `view`-mode share; messages are read straight from the latest checkpoint. `GET /{code}/files/{path}` - serves a file referenced by that conversation (images, Markdown, text, PDF, ?) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (`text/html`, `application/xhtml+xml`, `image/svg+xml`) is forced to download (`_public_share_disposition`); everything else is served inline. Missing/revoked/non-`view` codes all return 404. |
|
||||
| **Roundtable Drafts** (`/api/roundtable-drafts`) | **Strictly per-user** persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta — incl. `chain_title`, the business-chain name used this session, cheaply regex-extracted from the in-memory `step2` blob in `_draft_to_meta`/`_extract_chain_title`; null = no chain / 自由模式; surfaced on the 第一步「分析记录」history card + sidebar list instead of the old「第 X 步」); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete (all owner-scoped). A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). taskId 深链会商记录 are **not** here — they live in the separate `/api/roundtable-task-drafts` store (see below) so personal history never mixes with task records. Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. Tests: `tests/test_roundtable_task_drafts.py`. |
|
||||
@@ -393,7 +394,10 @@ query-time type, `recommended_question`.
|
||||
Sync only ever adds/removes `origin='position'` rows, so a user's own
|
||||
favorites/subscriptions are never clobbered. Default sets (no binding for a
|
||||
type): agents = built-ins (`user_id IS NULL`); skills = empty (square stays
|
||||
full, nothing added to 我的); scheduled tasks = empty; questions = unrestricted.
|
||||
full, nothing added to 我的); scheduled tasks = empty; questions = **untagged
|
||||
only** (默认岗位 / 无标签绑定 只看到未打标的题目;打标的题目只在绑定了对应标签的
|
||||
岗位展示 — the router interprets `resolve_visible → None` as "show only untagged",
|
||||
not "show everything").
|
||||
`origin`/`position_id` columns were added to `agent_favorites` and
|
||||
`scheduled_task_subscriptions` — see migration `20260616_01` +
|
||||
`scripts/sql/column_additions_mysql.sql`. The running app also auto-adds these
|
||||
@@ -415,8 +419,12 @@ unassigned users inherit, else `null`); powers the top-bar 岗位 label
|
||||
changes: `GET /api/recommended-questions` is now
|
||||
filtered by the caller's position (`?all=1` bypasses for admin management) and
|
||||
admin can tag questions via `/api/tags/assignments/recommended_question/{id}`.
|
||||
Each listed question now carries its assigned `tags` (bulk-attached via
|
||||
`tag_store.list_assignments`), and the list accepts a repeatable `tag_ids`
|
||||
**Default-position rule**: tags are bulk-attached to **all** questions first,
|
||||
then the visibility filter applies — a position binding recommended_question 标签
|
||||
sees only those tagged questions; a 默认岗位 / 无绑定 (`resolve_visible → None`)
|
||||
sees only the **untagged** questions (打标的题目不再泄漏到默认岗位). `?all=1`
|
||||
(admin management) skips this filter and returns every question with its tags.
|
||||
The list also accepts a repeatable `tag_ids`
|
||||
OR-filter — so the 推荐问题配置 page can display per-question 标签 and search by
|
||||
them (frontend `RecommendedQuestionsPage` + `TagSearchBar`). Tests:
|
||||
`tests/test_recommended_questions_tags.py`.
|
||||
@@ -520,6 +528,61 @@ Operators upgrading an existing DB run the whole file (`USE <db>;` then execute)
|
||||
- **Installation**: `POST /api/skills/install` extracts .skill ZIP archive to custom/ directory
|
||||
- **Chinese name (`name_zh`)**: a Chinese display name stored in the **`skills` DB table** (`SkillRow.name_zh`, `String(128)`, migration `20260609_01`) ? never written to the on-disk SKILL.md, so it works for built-in skills too. Threaded through `SkillStore` create/ensure_legacy/update/update_admin exactly like `detail`; surfaced on `SkillResponse.name_zh` from the ownership record in `_skill_to_response`. Authorization mirrors `detail`: custom skill ? owner or admin; built-in/public ? admin only (row materialized on demand via `ensure_legacy`). Endpoints (in `app/gateway/routers/skills.py`): `PUT /api/skills/{name}/name-zh` (set/clear), `POST /api/skills/{name}/name-zh/generate` (LLM generates one), `POST /api/skills/name-zh/generate-missing` (????: **admin-only** bulk-generate for every skill that lacks one, bounded-concurrency `asyncio.gather`). **Optional model selection**: both generate endpoints accept an optional JSON body `{model_name}` ? an explicit name wins, else the curator model setting, else the main model (`_resolve_skill_name_zh_model_name`). **Model-error surfacing**: `_generate_skill_name_zh` raises `SkillNameZhModelError` when the model itself fails (build/timeout/API error) ? distinct from the model returning an unusable name (? `None`). The single endpoint maps that to `502 ?????,?????????:?`; the bulk endpoint fails fast on a model-build error (502) and otherwise carries a representative `model_error` string in `SkillNameZhBulkResponse` so the UI can say "N ?????????". The frontend (`skill-settings-page.tsx` list + `skill-detail-dialog.tsx` editor, plus the agent skill pickers/badges in `NewAgentPage.tsx` + `agent-card.tsx`) shows `name_zh || name`, offers per-skill "AI ??" plus an **admin-only** header "???????" button, and a compact model dropdown (`components/workspace/model-name-select.tsx`, default = configured model) beside each action.
|
||||
|
||||
#### Skill Address Management (技能地址管理 — URL/IP 批量替换)
|
||||
|
||||
Admin-only maintenance tool to find every `http(s)://` URL and IPv4 (`ip[:port]`)
|
||||
address in the **running** skills (`skills/{public,custom}`, all `.md` + `.py`,
|
||||
recursing into `scripts/`/`references/`/`templates/`) and rewrite them in bulk —
|
||||
e.g. swapping a UAT endpoint for production across all skills at once. The same
|
||||
address commonly lives in both a skill's `SKILL.md` and its `scripts/*.py`, so
|
||||
results are **aggregated by unique address** with every occurrence (`file:line` +
|
||||
snippet) and replaced together.
|
||||
|
||||
- **Pure engine** — `deerflow/skills/address_scan.py` (harness, no `app.*`):
|
||||
`find_spans` / `scan_occurrences` (URL regex with trailing-punctuation strip;
|
||||
IPv4 with 0–255 octet check + optional port; an IP inside a URL is *not*
|
||||
separately surfaced), `apply_replacements` (**substring-safe**: re-detects exact
|
||||
token spans and only rewrites a span whose text equals a requested `from`, so
|
||||
`1.2.3.4` is never rewritten inside `11.2.3.40`), `infer_kind`,
|
||||
`is_valid_replacement`. Tested in `tests/test_skill_addresses.py`.
|
||||
- **Router** — `app/gateway/routers/skill_addresses.py` (`/api/skill-addresses`,
|
||||
**admin only**). **Availability at scale (150+ skills):** every filesystem
|
||||
walk/read/write runs via `asyncio.to_thread` so the event loop is never blocked.
|
||||
Routes: `GET /scan` (aggregated, one-shot) + `GET /scan/stream` (per-skill NDJSON
|
||||
progress `{type:start|progress|result}`); `POST /preview` (dry-run diff +
|
||||
validation warnings); `POST /apply` + `POST /apply/stream` (per-file NDJSON
|
||||
progress); `GET /backups`; `POST /rollback/{backup_id}`. **Apply safety:**
|
||||
optimistic lock via `expected_total` (409 if files changed since the scan),
|
||||
path-guard (`is_relative_to` the skills root), auto disk backup to
|
||||
`{skills_root}/.address-edits/{timestamp}/` (the `.`-prefixed dir is excluded
|
||||
from scans), then `refresh_skills_system_prompt_cache_async()` so edits take
|
||||
effect immediately. Registered in `app.py` after `sensitive_words.router`.
|
||||
**Per-edit history:** each apply also writes a sibling manifest
|
||||
`{skills_root}/.address-edits/{timestamp}.manifest.json` (best-effort) capturing
|
||||
`{created_at, replaced_total, replacements:[{from,to,kind}], changed_files}`; the
|
||||
sibling-not-child placement keeps it out of both the restore set and the file
|
||||
count. `GET /backups` returns each record enriched with `replaced_total` +
|
||||
`replacements` (from the manifest; falls back to dir mtime when absent), and
|
||||
`POST /rollback/{backup_id}` restores a single record — so the UI shows a full
|
||||
modification history and rolls back any one entry independently.
|
||||
- **Frontend** — the panel is the reusable `SkillAddressesPanel` (exported from
|
||||
`pages/SkillAddressesPage.tsx`; its default export keeps the standalone route
|
||||
`/page/strategy/admin/skill-addresses` as a direct-link fallback). It is
|
||||
**embedded as a「技能地址管理」tab inside the admin「技能管理」page**
|
||||
`pages/AdminSkillsPage.tsx` (Tabs: 技能列表 + 技能地址管理; reached from the
|
||||
「技能管理」item in the「设置和更多」dropdown `workspace-nav-menu.tsx` →
|
||||
`/page/workspace/admin/skills`) — it is no longer a separate entry in the
|
||||
`components/page-sidebar.tsx` settings dropdown. The panel:
|
||||
auto-scan on open with a **progress bar** (扫描逐技能 / 应用逐文件 X/N), URL/IP
|
||||
type filter, search, a「片段批量套用」helper (e.g. all `uat.4.cn` → `prod.4.cn`),
|
||||
per-address「替换为」input with expandable occurrences, inline 预览 panel, 应用,
|
||||
**multi-keyword search** (space-separated, all-must-hit), and a **修改记录**
|
||||
(modification history) panel — each apply is one record (time / which addresses
|
||||
changed from→to / file & replacement counts) with a per-record 回退此次
|
||||
(single-record rollback, with inline confirm). Client + NDJSON stream parsing in
|
||||
`strategy-components/api/skill-addresses.ts`. Scope is the running skills only;
|
||||
the `deploy/minimal/skills/` copy is not touched (sync separately).
|
||||
|
||||
#### Skill Compression (技能压缩)
|
||||
|
||||
When the skill catalogue grows large, injecting every enabled skill full-text
|
||||
@@ -615,6 +678,7 @@ scheduler runs once per deployment. Per run:
|
||||
|
||||
- `create_chat_model(name, thinking_enabled)` instantiates LLM from config via reflection
|
||||
- Supports `thinking_enabled` flag with per-model `when_thinking_enabled` overrides
|
||||
- `force_disable_thinking=True` (kwarg) hard-disables thinking even when the model config declares **no** `when_thinking_*`/`thinking` shape — many internal vLLM/Qwen models only set `supports_thinking: true`, so `thinking_enabled=False` alone injects nothing and they keep reasoning on. The flag injects both OpenAI-compatible disable shapes (`extra_body.chat_template_kwargs.enable_thinking=false` for vLLM/Qwen + `extra_body.thinking.type="disabled"` for Zhipu GLM), only for classes that accept `extra_body`. Mirrors `TitleMiddleware._force_disable_thinking`. The lead agent reads the runtime flag `thinking_force_disabled` and forwards it (used by `/api/open/chat`)
|
||||
- Supports vLLM-style thinking toggles via `when_thinking_enabled.extra_body.chat_template_kwargs.enable_thinking` for Qwen reasoning models, while normalizing legacy `thinking` configs for backward compatibility
|
||||
- Supports `supports_vision` flag for image understanding models
|
||||
- Config values starting with `$` resolved as environment variables
|
||||
|
||||
Reference in new issue
Block a user