Compare commits
156
Commits
feat/gen-ui
...
skill
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77e41a37ee | ||
|
|
79229b7ede | ||
|
|
36ae648a63 | ||
|
|
c409d531da | ||
|
|
511ca69e4e | ||
|
|
4c7e60d7e7 | ||
|
|
a9e163403d | ||
|
|
bc136b89f9 | ||
|
|
b1bd4f9a8b | ||
|
|
06d3927f05 | ||
|
|
5c3516cae8 | ||
|
|
1c18d8ddbd | ||
|
|
253dfe9351 | ||
|
|
2d3999e984 | ||
|
|
a431cc726d | ||
|
|
08493d2429 | ||
|
|
8dd0969006 | ||
|
|
2d80d3c35c | ||
|
|
4da764904c | ||
|
|
137d4f3f60 | ||
|
|
e723b348cf | ||
|
|
59fb603ec6 | ||
|
|
63f072ebfb | ||
|
|
0913d04d33 | ||
|
|
80ff108fc0 | ||
|
|
ea58129ebe | ||
|
|
698869460c | ||
|
|
f3f2f63110 | ||
|
|
b100cdc382 | ||
|
|
9e90b32991 | ||
|
|
b2d54cb2e9 | ||
|
|
1e5e47b889 | ||
|
|
dd28b0d165 | ||
|
|
49e74e2f58 | ||
|
|
e6e5d761c2 | ||
|
|
e1ce808567 | ||
|
|
3c7ad816d9 | ||
|
|
f44dd343da | ||
|
|
2d9b2d50f3 | ||
|
|
003e0f9614 | ||
|
|
da0979b373 | ||
|
|
81fb57639e | ||
|
|
1e61078e93 | ||
|
|
b597bb1b17 | ||
|
|
65ead05dfc | ||
|
|
6f8ed69ee1 | ||
|
|
59b879d2df | ||
|
|
a57b0b9b58 | ||
|
|
d8506efed6 | ||
|
|
b25b28cc76 | ||
|
|
f68cabe226 | ||
|
|
f73f83c5d6 | ||
|
|
29801d6a65 | ||
|
|
75016e8bfe | ||
|
|
b6dea7b2ba | ||
|
|
b2b1cd7e97 | ||
|
|
ca63e01d45 | ||
|
|
271b6e1f5c | ||
|
|
609ddf9e01 | ||
|
|
9fd1fea58c | ||
|
|
8de3b4d033 | ||
|
|
d9c038daf2 | ||
|
|
c9c057fd04 | ||
|
|
85189be53d | ||
|
|
01e736aaf7 | ||
|
|
a28969f564 | ||
|
|
42f9610ba5 | ||
|
|
1a7ffbe5ee | ||
|
|
481354ca70 | ||
|
|
3c80857ea3 | ||
|
|
94702efbc2 | ||
|
|
755f7b0e8b | ||
|
|
a1c9564b30 | ||
|
|
97acd9c70b | ||
|
|
71601bf76c | ||
|
|
460efbf285 | ||
|
|
a752f43b8e | ||
|
|
b3774bfe1c | ||
|
|
4313d23889 | ||
|
|
26a43db6a4 | ||
|
|
e5d40336b3 | ||
|
|
58e2bcbba1 | ||
|
|
e1b55f54e6 | ||
|
|
989039db6e | ||
|
|
52428df1bd | ||
|
|
28d174603f | ||
|
|
b6099d09ff | ||
|
|
2962a7ea93 | ||
|
|
cc01189966 | ||
|
|
d563d6e7df | ||
|
|
e4e99d198b | ||
|
|
34e6e5a049 | ||
|
|
7e6ba05464 | ||
|
|
aeda37db68 | ||
|
|
441f9f9ec0 | ||
|
|
f16c249b1e | ||
|
|
858b3b74dc | ||
|
|
d5c0e927c6 | ||
|
|
b81737cfb3 | ||
|
|
e5496e04c5 | ||
|
|
75c54bd40b | ||
|
|
119b6532fd | ||
|
|
c61d33f91e | ||
|
|
99c1cb7781 | ||
|
|
448e378fc0 | ||
|
|
8511a3143e | ||
|
|
fd2e26f8e6 | ||
|
|
1d34184999 | ||
|
|
ac0320a70d | ||
|
|
4a44befb00 | ||
|
|
5aa2092d0f | ||
|
|
99ed826a55 | ||
|
|
a45f862fb2 | ||
|
|
00496ec867 | ||
|
|
58592e1cb1 | ||
|
|
e6efbb08df | ||
|
|
662a77213d | ||
|
|
c2d560ea26 | ||
|
|
ca1782b105 | ||
|
|
8caa1d1379 | ||
|
|
c56ac3983e | ||
|
|
fa000ae8a7 | ||
|
|
8906a49e56 | ||
|
|
a2d9aeff37 | ||
|
|
aff5c47541 | ||
|
|
58f83fa7e7 | ||
|
|
2d5161a647 | ||
|
|
1227f19c6a | ||
|
|
a0b917425a | ||
|
|
799c7eea3a | ||
|
|
5277e4b375 | ||
|
|
a877e1bd7e | ||
|
|
a5703762c0 | ||
|
|
dcd40d603c | ||
|
|
8ef002f624 | ||
|
|
b1dec5bd42 | ||
|
|
4e85acc605 | ||
|
|
a2c67372ed | ||
|
|
7bb09ce039 | ||
|
|
e1495c0bae | ||
|
|
ededa1f4cd | ||
|
|
b2861b60d8 | ||
|
|
3cbe49bf45 | ||
|
|
dd19b4ff79 | ||
|
|
66a545fc6d | ||
|
|
eac9f69ad1 | ||
|
|
e4dc29f98e | ||
|
|
6889974c05 | ||
|
|
aef1c67721 | ||
|
|
3ab9e083e0 | ||
|
|
6e2c21f568 | ||
|
|
abbad7b313 | ||
|
|
c4bfea6096 | ||
|
|
bbb133cd58 | ||
|
|
34e2205efa | ||
|
|
db29caff2b |
@@ -1,168 +1,40 @@
|
||||
---
|
||||
name: backend-code-review
|
||||
description: Review backend code for quality, security, maintainability, and best practices based on established checklist rules. Use when the user requests a review, analysis, or improvement of backend files (e.g., `.py`) under the `api/` directory. Do NOT use for frontend files (e.g., `.tsx`, `.ts`, `.js`). Supports pending-change review, code snippets review, and file-focused review.
|
||||
description: Use only when the user explicitly requests a review or audit of backend code under `api/`. Supports pending-change, file-focused, and pasted-diff reviews. Do not use for implementation-only requests, diagnosis without review intent, frontend code, or backend code outside `api/`.
|
||||
---
|
||||
|
||||
# Backend Code Review
|
||||
|
||||
## When to use this skill
|
||||
Review the requested scope for concrete, reproducible defects. The nearest `AGENTS.md` owns package facts and commands; this skill owns the review workflow and routes to its bundled rule packs.
|
||||
|
||||
Use this skill whenever the user asks to **review, analyze, or improve** backend code (e.g., `.py`) under the `api/` directory. Supports the following review modes:
|
||||
## Evidence First
|
||||
|
||||
- **Pending-change review**: when the user asks to review current changes (inspect staged/working-tree files slated for commit to get the changes).
|
||||
- **Code snippets review**: when the user pastes code snippets (e.g., a function/class/module excerpt) into the chat and asks for a review.
|
||||
- **File-focused review**: when the user points to specific files and asks for a review of those files (one file or a small, explicit set of files, e.g., `api/...`, `api/app.py`).
|
||||
1. Establish the requested review scope and inspect the relevant diff or files.
|
||||
2. Read the changed lines, their behavior owner, nearby tests, and local docstrings or comments that define contracts.
|
||||
3. Trace callers, persistence boundaries, authorization, generated schemas, or external I/O only when they decide correctness.
|
||||
4. Report only findings tied to an observable failure, violated contract, security boundary, data integrity risk, or demonstrated maintenance problem.
|
||||
|
||||
Do NOT use this skill when:
|
||||
## Rule Routing
|
||||
|
||||
- The request is about frontend code or UI (e.g., `.tsx`, `.ts`, `.js`, `web/`).
|
||||
- The user is not asking for a review/analysis/improvement of backend code.
|
||||
- The scope is not under `api/` (unless the user explicitly asks to review backend-related changes outside `api/`).
|
||||
Read only the packs matched by the diff:
|
||||
|
||||
## How to use this skill
|
||||
- Models or migrations: [`references/db-schema-rule.md`][db-schema]
|
||||
- Controller, service, core/domain, library, or model dependency direction: [`references/architecture-rule.md`][architecture]
|
||||
- Table access outside an established repository boundary: [`references/repositories-rule.md`][repositories]
|
||||
- SQLAlchemy sessions, queries, transactions, CRUD, concurrency, or raw SQL: [`references/sqlalchemy-rule.md`][sqlalchemy]
|
||||
|
||||
Follow these steps when using this skill:
|
||||
When no pack applies, review correctness, security, behavior changes, and test evidence directly. Check current official documentation only when local code and contracts do not settle framework or library behavior.
|
||||
|
||||
1. **Identify the review mode** (pending-change vs snippet vs file-focused) based on the user’s input. Keep the scope tight: review only what the user provided or explicitly referenced.
|
||||
2. Follow the rules defined in **Checklist** to perform the review. If no Checklist rule matches, apply **General Review Rules** as a fallback to perform the best-effort review.
|
||||
3. Compose the final output strictly follow the **Required Output Format**.
|
||||
## Severity And Output
|
||||
|
||||
Notes when using this skill:
|
||||
- Always include actionable fixes or suggestions (including possible code snippets).
|
||||
- Use best-effort `File:Line` references when a file path and line numbers are available; otherwise, use the most specific identifier you can.
|
||||
- **P0**: security or privacy exposure, data loss, or a production-wide outage.
|
||||
- **P1**: user-visible regression, broken authorization or tenant isolation, invalid public contract, or failed primary workflow.
|
||||
- **P2**: concrete correctness, performance, maintainability, or test defect likely to cause incorrect behavior.
|
||||
- **P3**: minor actionable cleanup; omit unless the user requested a thorough audit.
|
||||
|
||||
## Checklist
|
||||
Lead with findings ordered by severity. Include a tight file and line reference, the failing contract or reproduction path, impact, and a concrete fix direction. If there are no findings, say `No issues found.` and state any material verification gap. Do not add praise sections, speculative risks, or an unsolicited offer to implement fixes.
|
||||
|
||||
- db schema design: if the review scope includes code/files under `api/models/` or `api/migrations/`, follow [references/db-schema-rule.md](references/db-schema-rule.md) to perform the review
|
||||
- architecture: if the review scope involves controller/service/core-domain/libs/model layering, dependency direction, or moving responsibilities across modules, follow [references/architecture-rule.md](references/architecture-rule.md) to perform the review
|
||||
- repositories abstraction: if the review scope contains table/model operations (e.g., `select(...)`, `session.execute(...)`, joins, CRUD) and is not under `api/repositories`, `api/core/repositories`, or `api/extensions/*/repositories/`, follow [references/repositories-rule.md](references/repositories-rule.md) to perform the review
|
||||
- sqlalchemy patterns: if the review scope involves SQLAlchemy session/query usage, db transaction/crud usage, or raw SQL usage, follow [references/sqlalchemy-rule.md](references/sqlalchemy-rule.md) to perform the review
|
||||
|
||||
## General Review Rules
|
||||
|
||||
### 1. Security Review
|
||||
|
||||
Check for:
|
||||
- SQL injection vulnerabilities
|
||||
- Server-Side Request Forgery (SSRF)
|
||||
- Command injection
|
||||
- Insecure deserialization
|
||||
- Hardcoded secrets/credentials
|
||||
- Improper authentication/authorization
|
||||
- Insecure direct object references
|
||||
|
||||
### 2. Performance Review
|
||||
|
||||
Check for:
|
||||
- N+1 queries
|
||||
- Missing database indexes
|
||||
- Memory leaks
|
||||
- Blocking operations in async code
|
||||
- Missing caching opportunities
|
||||
|
||||
### 3. Code Quality Review
|
||||
|
||||
Check for:
|
||||
- Code forward compatibility
|
||||
- Code duplication (DRY violations)
|
||||
- Functions doing too much (SRP violations)
|
||||
- Deep nesting / complex conditionals
|
||||
- Magic numbers/strings
|
||||
- Poor naming
|
||||
- Missing error handling
|
||||
- Incomplete type coverage
|
||||
|
||||
### 4. Testing Review
|
||||
|
||||
Check for:
|
||||
- Missing test coverage for new code
|
||||
- Tests that don't test behavior
|
||||
- Flaky test patterns
|
||||
- Missing edge cases
|
||||
|
||||
## Required Output Format
|
||||
|
||||
When this skill invoked, the response must exactly follow one of the two templates:
|
||||
|
||||
### Template A (any findings)
|
||||
|
||||
```markdown
|
||||
# Code Review Summary
|
||||
|
||||
Found <X> critical issues need to be fixed:
|
||||
|
||||
## 🔴 Critical (Must Fix)
|
||||
|
||||
### 1. <brief description of the issue>
|
||||
|
||||
FilePath: <path> line <line>
|
||||
<relevant code snippet or pointer>
|
||||
|
||||
#### Explanation
|
||||
|
||||
<detailed explanation and references of the issue>
|
||||
|
||||
#### Suggested Fix
|
||||
|
||||
1. <brief description of suggested fix>
|
||||
2. <code example> (optional, omit if not applicable)
|
||||
|
||||
---
|
||||
... (repeat for each critical issue) ...
|
||||
|
||||
Found <Y> suggestions for improvement:
|
||||
|
||||
## 🟡 Suggestions (Should Consider)
|
||||
|
||||
### 1. <brief description of the suggestion>
|
||||
|
||||
FilePath: <path> line <line>
|
||||
<relevant code snippet or pointer>
|
||||
|
||||
#### Explanation
|
||||
|
||||
<detailed explanation and references of the suggestion>
|
||||
|
||||
#### Suggested Fix
|
||||
|
||||
1. <brief description of suggested fix>
|
||||
2. <code example> (optional, omit if not applicable)
|
||||
|
||||
---
|
||||
... (repeat for each suggestion) ...
|
||||
|
||||
Found <Z> optional nits:
|
||||
|
||||
## 🟢 Nits (Optional)
|
||||
### 1. <brief description of the nit>
|
||||
|
||||
FilePath: <path> line <line>
|
||||
<relevant code snippet or pointer>
|
||||
|
||||
#### Explanation
|
||||
|
||||
<explanation and references of the optional nit>
|
||||
|
||||
#### Suggested Fix
|
||||
|
||||
- <minor suggestions>
|
||||
|
||||
---
|
||||
... (repeat for each nits) ...
|
||||
|
||||
## ✅ What's Good
|
||||
|
||||
- <Positive feedback on good patterns>
|
||||
```
|
||||
|
||||
- If there are no critical issues or suggestions or option nits or good points, just omit that section.
|
||||
- If the issue number is more than 10, summarize as "Found 10+ critical issues/suggestions/optional nits" and only output the first 10 items.
|
||||
- Don't compress the blank lines between sections; keep them as-is for readability.
|
||||
- If there is any issue requires code changes, append a brief follow-up question to ask whether the user wants to apply the fix(es) after the structured output. For example: "Would you like me to use the Suggested fix(es) to address these issues?"
|
||||
|
||||
### Template B (no issues)
|
||||
|
||||
```markdown
|
||||
## Code Review Summary
|
||||
✅ No issues found.
|
||||
```
|
||||
[architecture]: references/architecture-rule.md
|
||||
[db-schema]: references/db-schema-rule.md
|
||||
[repositories]: references/repositories-rule.md
|
||||
[sqlalchemy]: references/sqlalchemy-rule.md
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
### Keep business logic out of controllers
|
||||
- Category: maintainability
|
||||
- Severity: critical
|
||||
- Description: Controllers should parse input, call services, and return serialized responses. Business decisions inside controllers make behavior hard to reuse and test.
|
||||
- Suggested fix: Move domain/business logic into the service or core/domain layer. Keep controller handlers thin and orchestration-focused.
|
||||
- Example:
|
||||
@@ -34,7 +33,6 @@
|
||||
|
||||
### Preserve layer dependency direction
|
||||
- Category: best practices
|
||||
- Severity: critical
|
||||
- Description: Controllers may depend on services, and services may depend on core/domain abstractions. Reversing this direction (for example, core importing controller/web modules) creates cycles and leaks transport concerns into domain code.
|
||||
- Suggested fix: Extract shared contracts into core/domain or service-level modules and make upper layers depend on lower, not the reverse.
|
||||
- Example:
|
||||
@@ -58,7 +56,6 @@
|
||||
|
||||
### Keep libs business-agnostic
|
||||
- Category: maintainability
|
||||
- Severity: critical
|
||||
- Description: Modules under `api/libs/` should remain reusable, business-agnostic building blocks. They must not encode product/domain-specific rules, workflow orchestration, or business decisions.
|
||||
- Suggested fix:
|
||||
- If business logic appears in `api/libs/`, extract it into the appropriate `services/` or `core/` module and keep `libs` focused on generic, cross-cutting helpers.
|
||||
@@ -88,4 +85,4 @@
|
||||
def should_archive_conversation(conversation, tenant_id: str) -> bool:
|
||||
threshold_days = 90 if has_paid_plan(tenant_id) else 30
|
||||
return older_than_days(conversation.idle_days, threshold_days)
|
||||
```
|
||||
```
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
### Do not query other tables inside `@property`
|
||||
- Category: [maintainability, performance]
|
||||
- Severity: critical
|
||||
- Description: A model `@property` must not open sessions or query other tables. This hides dependencies across models, tightly couples schema objects to data access, and can cause N+1 query explosions when iterating collections.
|
||||
- Suggested fix:
|
||||
- Keep model properties pure and local to already-loaded fields.
|
||||
@@ -41,7 +40,6 @@
|
||||
|
||||
### Prefer including `tenant_id` in model definitions
|
||||
- Category: maintainability
|
||||
- Severity: suggestion
|
||||
- Description: In multi-tenant domains, include `tenant_id` in schema definitions whenever the entity belongs to tenant-owned data. This improves data isolation safety and keeps future partitioning/sharding strategies practical as data volume grows.
|
||||
- Suggested fix:
|
||||
- Add a `tenant_id` column and ensure related unique/index constraints include tenant dimension when applicable.
|
||||
@@ -70,7 +68,6 @@
|
||||
|
||||
### Detect and avoid duplicate/redundant indexes
|
||||
- Category: performance
|
||||
- Severity: suggestion
|
||||
- Description: Review index definitions for leftmost-prefix redundancy. For example, index `(a, b, c)` can safely cover most lookups for `(a, b)`. Keeping both may increase write overhead and can mislead the optimizer into suboptimal execution plans.
|
||||
- Suggested fix:
|
||||
- Before adding an index, compare against existing composite indexes by leftmost-prefix rules.
|
||||
@@ -94,7 +91,6 @@
|
||||
|
||||
### Avoid PostgreSQL-only dialect usage in models; wrap in `models.types`
|
||||
- Category: maintainability
|
||||
- Severity: critical
|
||||
- Description: Model/schema definitions should avoid PostgreSQL-only constructs directly in business models. When database-specific behavior is required, encapsulate it in `api/models/types.py` using both PostgreSQL and MySQL dialect implementations, then consume that abstraction from model code.
|
||||
- Suggested fix:
|
||||
- Do not directly place dialect-only types/operators in model columns when a portable wrapper can be used.
|
||||
@@ -122,7 +118,6 @@
|
||||
|
||||
### Guard migration incompatibilities with dialect checks and shared types
|
||||
- Category: maintainability
|
||||
- Severity: critical
|
||||
- Description: Migration scripts under `api/migrations/versions/` must account for PostgreSQL/MySQL incompatibilities explicitly. For dialect-sensitive DDL or defaults, branch on the active dialect (for example, `conn.dialect.name == "postgresql"`), and prefer reusable compatibility abstractions from `models.types` where applicable.
|
||||
- Suggested fix:
|
||||
- In migration upgrades/downgrades, bind connection and branch by dialect for incompatible SQL fragments.
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
### Introduce repositories abstraction
|
||||
- Category: maintainability
|
||||
- Severity: suggestion
|
||||
- Description: If a table/model already has a repository abstraction, all reads/writes/queries for that table should use the existing repository. If no repository exists, introduce one only when complexity justifies it, such as large/high-volume tables, repeated complex query logic, or likely storage-strategy variation.
|
||||
- Suggested fix:
|
||||
- First check `api/repositories`, `api/core/repositories`, and `api/extensions/*/repositories/` to verify whether the table/model already has a repository abstraction. If it exists, route all operations through it and add missing repository methods instead of bypassing it with ad-hoc SQLAlchemy access.
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
### Use Session context manager with explicit transaction control behavior
|
||||
- Category: best practices
|
||||
- Severity: critical
|
||||
- Description: Session and transaction lifecycle must be explicit and bounded on write paths. Missing commits can silently drop intended updates, while ad-hoc or long-lived transactions increase contention, lock duration, and deadlock risk.
|
||||
- Suggested fix:
|
||||
- Use **explicit `session.commit()`** after completing a related write unit.
|
||||
@@ -47,7 +46,6 @@
|
||||
|
||||
### Enforce tenant_id scoping on shared-resource queries
|
||||
- Category: security
|
||||
- Severity: critical
|
||||
- Description: Reads and writes against shared tables must be scoped by `tenant_id` to prevent cross-tenant data leakage or corruption.
|
||||
- Suggested fix: Add `tenant_id` predicate to all tenant-owned entity queries and propagate tenant context through service/repository interfaces.
|
||||
- Example:
|
||||
@@ -67,7 +65,6 @@
|
||||
|
||||
### Prefer SQLAlchemy expressions over raw SQL by default
|
||||
- Category: maintainability
|
||||
- Severity: suggestion
|
||||
- Description: Raw SQL should be exceptional. ORM/Core expressions are easier to evolve, safer to compose, and more consistent with the codebase.
|
||||
- Suggested fix: Rewrite straightforward raw SQL into SQLAlchemy `select/update/delete` expressions; keep raw SQL only when required by clear technical constraints.
|
||||
- Example:
|
||||
@@ -89,7 +86,6 @@
|
||||
|
||||
### Protect write paths with concurrency safeguards
|
||||
- Category: quality
|
||||
- Severity: critical
|
||||
- Description: Multi-writer paths without explicit concurrency control can silently overwrite data. Choose the safeguard based on contention level, lock scope, and throughput cost instead of defaulting to one strategy.
|
||||
- Suggested fix:
|
||||
- **Optimistic locking**: Use when contention is usually low and retries are acceptable. Add a version (or updated_at) guard in `WHERE` and treat `rowcount == 0` as a conflict.
|
||||
@@ -136,4 +132,4 @@
|
||||
).scalar_one()
|
||||
run.status = "cancelled"
|
||||
session.commit()
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,87 +1,31 @@
|
||||
---
|
||||
name: e2e-cucumber-playwright
|
||||
description: Write, update, or review Dify end-to-end tests under `e2e/` that use Cucumber, Gherkin, and Playwright. Use when the task involves `.feature` files, `features/step-definitions/`, `features/support/`, `DifyWorld`, scenario tags, locator/assertion choices, or E2E testing best practices for this repository.
|
||||
description: Use when writing, changing, or reviewing Cucumber and Playwright tests under `e2e/`, including feature files, step definitions, support code, scenario tags, locators, and assertions. Do not use for Vitest, React Testing Library, backend tests, or generic browser automation outside the E2E suite.
|
||||
---
|
||||
|
||||
# Dify E2E Cucumber + Playwright
|
||||
# E2E Cucumber And Playwright
|
||||
|
||||
Use this skill for Dify's repository-level E2E suite in `e2e/`. Use [`e2e/AGENTS.md`](../../../e2e/AGENTS.md) as the canonical package guide for local architecture and conventions, then read any feature-scoped `AGENTS.md` that owns the target area. Apply Playwright/Cucumber best practices only where they fit the current suite.
|
||||
`e2e/AGENTS.md` owns the suite architecture, lifecycle, commands, tags, generated-client boundaries, fixtures, and cleanup contracts. Read the nearest feature-scoped `AGENTS.md` when one exists. This skill adds no parallel package policy.
|
||||
|
||||
## Scope
|
||||
## Topic Routing
|
||||
|
||||
- Use this skill for `.feature` files, Cucumber step definitions, `DifyWorld`, hooks, tags, and E2E review work under `e2e/`.
|
||||
- Do not use this skill for Vitest or React Testing Library work under `web/`; use `frontend-testing` instead.
|
||||
- Do not use this skill for backend test or API review tasks under `api/`.
|
||||
Read only the bundled reference required by the change:
|
||||
|
||||
## Read Order
|
||||
- Locator, assertion, isolation, or waiting decisions: [`references/playwright-best-practices.md`][playwright]
|
||||
- Scenario wording, step granularity, expressions, or tag design: [`references/cucumber-best-practices.md`][cucumber]
|
||||
|
||||
1. Read [`e2e/AGENTS.md`](../../../e2e/AGENTS.md) first.
|
||||
2. Read only the files directly involved in the task:
|
||||
- target `.feature` files under `e2e/features/`
|
||||
- related step files under `e2e/features/step-definitions/`
|
||||
- `e2e/features/support/hooks.ts` and `e2e/features/support/world.ts` when session lifecycle or shared state matters
|
||||
- `e2e/scripts/run-cucumber.ts` and `e2e/cucumber.config.ts` when tags or execution flow matter
|
||||
3. Read [`references/playwright-best-practices.md`](references/playwright-best-practices.md) only when locator, assertion, isolation, or waiting choices are involved.
|
||||
4. Read [`references/cucumber-best-practices.md`](references/cucumber-best-practices.md) only when scenario wording, step granularity, tags, or expression design are involved.
|
||||
5. Re-check official Playwright or Cucumber docs with the available documentation tools before introducing a new framework pattern.
|
||||
|
||||
Keep this skill focused on Cucumber, Playwright, and package-level E2E guidance. Put feature-specific conventions in the owning feature's `AGENTS.md` instead of adding them here.
|
||||
|
||||
## Local Rules
|
||||
|
||||
- `e2e/` uses Cucumber for scenarios and Playwright as the browser layer.
|
||||
- `DifyWorld` is the per-scenario context object. Type `this` as `DifyWorld` and use `async function`, not arrow functions.
|
||||
- Keep glue organized by capability under `e2e/features/step-definitions/`; use `common/` only for broadly reusable steps.
|
||||
- Treat `e2e/AGENTS.md`, `features/support/hooks.ts`, and the Cucumber configuration as the owners of current session and tag semantics. Verify them when behavior depends on session state instead of copying a tag inventory into this skill.
|
||||
- Do not import Playwright Test runner patterns that bypass the current Cucumber + `DifyWorld` architecture unless the task is explicitly about changing that architecture.
|
||||
- Perform the behavior under test through Playwright. APIs are allowed for setup, seed preparation, persistence polling, and cleanup, but ordinary Console JSON and representable multipart operations must use the scenario- or process-owned generated oRPC client with request and response validation enabled. Keep the setup/cleanup API identity independent from an unauthenticated or logged-out behavior browser.
|
||||
- Consume generated operations directly. Do not add one-to-one API wrappers, handwritten endpoint URLs, response DTO casts, duplicate schemas, global mutable clients, or TanStack Query caching in Cucumber. Keep helpers only for real fixture construction, multi-operation orchestration, invariants, polling, derived test views, or protocol adapters.
|
||||
- Keep SSE, binary, redirect-only, external-service, and readiness exceptions centralized under their protocol owner. A contract mismatch must fail and be fixed at the backend schema owner followed by regeneration; never weaken validation to make E2E pass.
|
||||
Check current official Playwright or Cucumber documentation before introducing a framework pattern that local code and references do not already establish.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Rebuild local context.
|
||||
- Inspect the target feature area.
|
||||
- Reuse an existing step when wording and behavior already match.
|
||||
- Add a new step only for a genuinely new user action or assertion.
|
||||
- Before adding several similar steps, scan the target capability for an existing domain noun that can be parameterized without hiding behavior.
|
||||
- Keep edits close to the current capability folder unless the step is broadly reusable.
|
||||
2. Write behavior-first scenarios.
|
||||
- Describe user-observable behavior, not DOM mechanics.
|
||||
- Keep each scenario focused on one workflow or outcome.
|
||||
- Keep scenarios independent and re-runnable.
|
||||
3. Write step definitions in the local style.
|
||||
- Keep one step to one user-visible action or one assertion.
|
||||
- Prefer Cucumber Expressions such as `{string}` and `{int}`.
|
||||
- Use a bounded regex only when the accepted values are a small explicit domain set and Cucumber Expressions would make the Gherkin less natural.
|
||||
- Do not create one-off steps for each case variant when the same domain action or outcome applies to named surfaces, modes, or resources.
|
||||
- Scope locators to stable containers when the page has repeated elements.
|
||||
- Avoid page-object layers or extra helper abstractions unless repeated complexity clearly justifies them.
|
||||
4. Use Playwright in the local style.
|
||||
- Prefer user-facing locators: `getByRole`, `getByLabel`, `getByPlaceholder`, `getByText`, then `getByTestId` for explicit contracts.
|
||||
- Use web-first `expect(...)` assertions.
|
||||
- Do not use `waitForTimeout`, manual polling, or raw visibility checks when a locator action or retrying assertion already expresses the behavior.
|
||||
- Use `expect.poll` for API persistence, backend eventual consistency, captured browser events, or other non-DOM state; prefer locator assertions for DOM readiness and visible UI state.
|
||||
- If a product element has real user-facing semantics but no accessible name, prefer fixing that accessible contract over adding a test id.
|
||||
5. Validate narrowly.
|
||||
- Run the narrowest tagged scenario or flow that exercises the change.
|
||||
- Run the package-required static checks documented in `e2e/AGENTS.md`.
|
||||
- Broaden verification only when the change affects hooks, tags, setup, or shared step semantics.
|
||||
1. Add E2E coverage only for a critical user journey with a cross-boundary outcome that cheaper owner-level tests do not already prove.
|
||||
2. Identify the user-visible behavior and its feature owner. Start from real product defaults and actor roles; setup may establish preconditions but must not manufacture the opposite state to make the scenario meaningful.
|
||||
3. Read the target scenario, matching step definitions, and lifecycle files only when session or shared state matters.
|
||||
4. Reuse an existing step when wording and behavior match; add one coherent scenario or step when they do not.
|
||||
5. Keep browser actions and assertions at the public user boundary; keep setup, seed, polling, and cleanup at their package-defined owners.
|
||||
6. Run the narrowest tagged scenario and package checks documented in `e2e/AGENTS.md`; broaden only for shared hooks, tags, or support changes.
|
||||
|
||||
## Review Checklist
|
||||
For review requests, lead with reproducible correctness failures, flake sources, or demonstrated architecture drift. Report the behavior verified and any external-runtime, browser, or environment gap.
|
||||
|
||||
- Does the scenario describe behavior rather than implementation?
|
||||
- Does it fit the current session model, tags, and `DifyWorld` usage?
|
||||
- Should an existing step be reused instead of adding a new one?
|
||||
- Are locators user-facing and assertions web-first?
|
||||
- Does the change introduce hidden coupling across scenarios, tags, or instance state?
|
||||
- Does it document or implement behavior that differs from the real hooks or configuration?
|
||||
- Does setup/cleanup use the generated client directly, with any remaining helper owning more than a one-to-one endpoint forward?
|
||||
- Is every raw HTTP call a documented protocol or infrastructure exception rather than an ordinary Console operation?
|
||||
|
||||
Lead findings with correctness, flake risk, and architecture drift.
|
||||
|
||||
## References
|
||||
|
||||
- [`references/playwright-best-practices.md`](references/playwright-best-practices.md)
|
||||
- [`references/cucumber-best-practices.md`](references/cucumber-best-practices.md)
|
||||
[cucumber]: references/cucumber-best-practices.md
|
||||
[playwright]: references/playwright-best-practices.md
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Cucumber Best Practices For Dify E2E
|
||||
# Cucumber Best Practices
|
||||
|
||||
Use this reference when writing or reviewing Gherkin scenarios, step definitions, parameter expressions, and step reuse in Dify's `e2e/` suite.
|
||||
Use this reference when writing or reviewing Gherkin scenarios, step definitions, parameter expressions, and step reuse.
|
||||
|
||||
Official sources:
|
||||
|
||||
- https://cucumber.io/docs/guides/10-minute-tutorial/
|
||||
- https://cucumber.io/docs/cucumber/step-definitions/
|
||||
- https://cucumber.io/docs/cucumber/cucumber-expressions/
|
||||
- https://cucumber.io/docs/guides/10-minute-tutorial
|
||||
- https://cucumber.io/docs/cucumber/step-definitions
|
||||
- https://cucumber.io/docs/cucumber/cucumber-expressions
|
||||
|
||||
## What Matters Most
|
||||
|
||||
@@ -24,11 +24,7 @@ Apply it like this:
|
||||
|
||||
A scenario should usually prove one workflow or business outcome. If a scenario wanders across several unrelated behaviors, split it.
|
||||
|
||||
In Dify's suite, this means:
|
||||
|
||||
- one capability-focused scenario per feature path
|
||||
- no long setup chains when existing bootstrap or reusable steps already cover them
|
||||
- no hidden dependency on another scenario's side effects
|
||||
Keep each scenario centered on one coherent outcome. Avoid hidden dependencies on another scenario's side effects, and keep unavoidable setup outside the behavior narrative unless the precondition matters to the specification.
|
||||
|
||||
### 3. Reuse steps, but only when behavior really matches
|
||||
|
||||
@@ -68,26 +64,11 @@ Use regex for a bounded natural-language alternative only when it keeps Gherkin
|
||||
|
||||
Step definitions are glue between Gherkin and automation, not a second abstraction language.
|
||||
|
||||
For Dify:
|
||||
|
||||
- type `this` as `DifyWorld`
|
||||
- use `async function`
|
||||
- keep each step to one user-visible action or assertion
|
||||
- rely on `DifyWorld` and existing support code for shared context
|
||||
- avoid leaking cross-scenario state
|
||||
Keep each step to one user-visible action or assertion. In JavaScript and TypeScript, use `async function` when the step reads Cucumber World state because Cucumber binds `this`; do not leak state across scenarios through module globals.
|
||||
|
||||
### 6. Use tags intentionally
|
||||
|
||||
Tags should communicate run scope or session semantics, not become ad hoc metadata.
|
||||
|
||||
In Dify's current suite:
|
||||
|
||||
- capability tags group related scenarios
|
||||
- `@unauthenticated` changes session behavior
|
||||
- `@authenticated` is descriptive/selective, not a behavior switch by itself
|
||||
- `@fresh` belongs to reset/full-install flows only
|
||||
|
||||
If a proposed tag implies behavior, verify that hooks or runner configuration actually implement it.
|
||||
Tags should communicate selection or execution intent, not become ad hoc metadata. A tag does not change runtime behavior unless configuration or hooks implement it.
|
||||
|
||||
## Review Questions
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Playwright Best Practices For Dify E2E
|
||||
# Playwright Best Practices
|
||||
|
||||
Use this reference when writing or reviewing locator, assertion, isolation, or synchronization logic for Dify's Cucumber-based E2E suite.
|
||||
Use this reference when writing or reviewing locator, assertion, isolation, or synchronization logic.
|
||||
|
||||
Official sources:
|
||||
|
||||
@@ -13,20 +13,19 @@ Official sources:
|
||||
|
||||
### 1. Keep scenarios isolated
|
||||
|
||||
Playwright's model is built around clean browser contexts so one test does not leak into another. In Dify's suite, that principle maps to per-scenario session setup in `features/support/hooks.ts` and `DifyWorld`.
|
||||
Playwright's model is built around clean browser contexts so one test does not leak into another.
|
||||
|
||||
Apply it like this:
|
||||
|
||||
- do not depend on another scenario having run first
|
||||
- do not persist ad hoc scenario state outside `DifyWorld`
|
||||
- do not couple ordinary scenarios to `@fresh` behavior
|
||||
- when a flow needs special auth/session semantics, express that through the existing tag model or explicit hook changes
|
||||
- keep scenario state in the runner's scenario-owned context rather than module globals
|
||||
- model special authentication or session setup through explicit per-scenario fixtures rather than shared mutable state
|
||||
|
||||
### 2. Prefer user-facing locators
|
||||
|
||||
Playwright recommends built-in locators that reflect what users perceive on the page.
|
||||
|
||||
Preferred order in this repository:
|
||||
Preferred order:
|
||||
|
||||
1. `getByRole`
|
||||
2. `getByLabel`
|
||||
@@ -79,16 +78,9 @@ Bad pattern:
|
||||
- stack arbitrary waits before every action
|
||||
- wait on unstable implementation details instead of the visible state the user cares about
|
||||
|
||||
### 5. Match debugging to the current suite
|
||||
### 5. Match debugging to the active harness
|
||||
|
||||
Playwright's wider ecosystem supports traces and rich debugging tools. Dify's current suite already captures:
|
||||
|
||||
- full-page screenshots
|
||||
- page HTML
|
||||
- console errors
|
||||
- page errors
|
||||
|
||||
Use the existing artifact flow by default. If a task is specifically about improving diagnostics, confirm the change fits the current Cucumber architecture before importing broader Playwright tooling.
|
||||
Playwright supports traces, screenshots, page snapshots, and browser logs. Configure artifact capture at the runner boundary instead of adding parallel diagnostics to individual scenarios.
|
||||
|
||||
## Review Questions
|
||||
|
||||
@@ -96,4 +88,4 @@ Use the existing artifact flow by default. If a task is specifically about impro
|
||||
- Is this assertion using Playwright's retrying semantics?
|
||||
- Is any explicit wait masking a real readiness problem?
|
||||
- Does this code preserve per-scenario isolation?
|
||||
- Is a new abstraction really needed, or does it bypass the existing `DifyWorld` + step-definition model?
|
||||
- Is a new abstraction really needed, or does it bypass the runner's scenario-owned context and lifecycle?
|
||||
|
||||
@@ -1,94 +1,48 @@
|
||||
---
|
||||
name: frontend-code-review
|
||||
description: Review Dify frontend code for correctness, accessibility, component design, dify-ui usage, data/query boundaries, performance, and tests. Trigger for `.tsx`, `.ts`, `.js`, UI, React, Next.js, pending-change, or focused frontend review requests.
|
||||
description: Use only when the user explicitly requests a review or audit of frontend code under `web/` or `packages/dify-ui/`. Supports pending-change, file-focused, and pasted-diff reviews. Do not use for implementation-only requests, diagnosis without review intent, or backend-only code.
|
||||
---
|
||||
|
||||
# Frontend Code Review
|
||||
|
||||
## When To Use
|
||||
Review the requested scope for concrete, reproducible regressions. This skill owns the review phase and routes directly to its bundled rule packs. For a combined review-and-fix request, establish findings before applying implementation or testing guidance.
|
||||
|
||||
Use this skill when the user asks to review, audit, analyze, or sanity-check frontend code under `web/`, `packages/dify-ui/`, or frontend-adjacent TypeScript files.
|
||||
## Evidence First
|
||||
|
||||
Supported modes:
|
||||
1. Establish the review scope from the requested files or current diff.
|
||||
2. Read the changed lines, their behavior owner, and the nearest scoped `AGENTS.md`.
|
||||
3. Trace public consumers, generated contracts, primitive APIs, or runtime configuration only when they decide correctness.
|
||||
4. Report only findings tied to an observable failure, violated contract, security boundary, or demonstrated maintenance risk.
|
||||
|
||||
- **Pending-change review**: inspect staged and working-tree changes.
|
||||
- **File-focused review**: inspect explicitly named files or paths.
|
||||
- **Diff/snippet review**: review pasted diffs or snippets using best-effort references.
|
||||
## Rule Routing
|
||||
|
||||
Do not use this skill for backend-only code under `api/`; use `backend-code-review` instead.
|
||||
Read only the packs matched by the diff:
|
||||
|
||||
## Required Context
|
||||
- DOM semantics, focus, keyboard, forms, disabled state, or visible interaction: [`references/accessibility-ui.md`][accessibility]
|
||||
- Dify UI imports, Base UI wrappers, overlays, tokens, or primitive contracts: [`references/dify-ui.md`][dify-ui]
|
||||
- Component ownership, props, state, Effects, navigation, or module boundaries: [`references/component-architecture.md`][component-architecture]
|
||||
- Generated clients, Query, mutations, auth, SSR, URL state, or persistence: [`references/data-query-contracts.md`][data-query]
|
||||
- Test files or a concrete missing-regression-test finding: [`references/testing.md`][testing]
|
||||
- Bundle, waterfall, rendering, or subscription cost supported by evidence: [`references/performance.md`][performance]
|
||||
- Stable Dify runtime invariants in the named paths: [`references/dify-invariants.md`][dify-invariants]
|
||||
- General TypeScript or styling quality not owned above: [`references/code-quality.md`][code-quality]
|
||||
|
||||
Before reviewing, read the relevant local contracts:
|
||||
Read `packages/dify-ui/README.md`, `packages/dify-ui/AGENTS.md`, `web/docs/overlay.md`, or `web/docs/test.md` only when the reviewed code falls under that contract. Check current official documentation when local code and bundled references do not settle a framework, browser, or accessibility behavior.
|
||||
|
||||
- `web/AGENTS.md` for Dify frontend workflow, overlays, design tokens, state, and tests.
|
||||
- `packages/dify-ui/README.md` and `packages/dify-ui/AGENTS.md` when code uses or changes `@langgenius/dify-ui/*`.
|
||||
- `web/docs/overlay.md` when reviewing dialogs, drawers, popovers, tooltips, menus, selects, comboboxes, or other floating UI.
|
||||
- `web/docs/test.md` and the `frontend-testing` skill when reviewing tests or testability.
|
||||
- `karpathy-guidelines` for scope control and focused, verifiable changes.
|
||||
- `how-to-write-component` when reviewing React component structure, ownership, effects, query/mutation contracts, or memoization.
|
||||
## Severity And Output
|
||||
|
||||
For any UI, UX, or accessibility review, fetch the latest Web Interface Guidelines before finalizing findings. Treat them as a required baseline, not the complete source of accessibility truth:
|
||||
- **P0**: security or privacy leak, data loss, production crash, or inaccessible critical workflow.
|
||||
- **P1**: user-visible regression, invalid API or authorization contract, hydration failure, or broken primary interaction.
|
||||
- **P2**: concrete maintainability, performance, test, or accessibility defect likely to cause incorrect behavior.
|
||||
- **P3**: minor actionable cleanup; omit unless the user requested a thorough audit.
|
||||
|
||||
```text
|
||||
https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md
|
||||
```
|
||||
Lead with findings ordered by severity. Include a tight file and line reference, the failing contract or reproduction path, impact, and a concrete fix direction. If there are no findings, say `No issues found.` and state any material verification gap. Do not add praise sections, speculative risks, or an unsolicited offer to implement fixes.
|
||||
|
||||
If the review depends on a current framework, SDK, browser API, or accessibility behavior and local code does not settle it, check the current official docs first. For browser compatibility, deprecation, or behavior-sensitive frontend APIs, verify MDN or the relevant standard.
|
||||
|
||||
## Rule Packs
|
||||
|
||||
Apply every relevant rule pack:
|
||||
|
||||
- [references/accessibility-ui.md](references/accessibility-ui.md) — accessibility, semantic HTML, focus, forms, keyboard, disabled states, copy, and long-content behavior. Combines Web Interface Guidelines with Dify UI, Base UI, MDN, and local primitive contracts.
|
||||
- [references/dify-ui.md](references/dify-ui.md) — Dify UI primitive usage, Base UI semantics, overlays, forms, tokens, radius mapping, and primitive boundaries.
|
||||
- [references/component-architecture.md](references/component-architecture.md) — component ownership, props, state, effects, exports, wrappers, and feature organization.
|
||||
- [references/data-query-contracts.md](references/data-query-contracts.md) — generated contracts, TanStack Query, mutations, workspace/auth/SSR boundaries, URL/local storage state.
|
||||
- [references/performance.md](references/performance.md) — React/Next performance review rules from Vercel guidance, scoped to real risk.
|
||||
- [references/testing.md](references/testing.md) — frontend test review rules.
|
||||
- [references/dify-invariants.md](references/dify-invariants.md) — stable Dify-specific runtime invariants that generic React/a11y rules will not catch.
|
||||
- [references/code-quality.md](references/code-quality.md) — general TypeScript, styling, naming, and maintainability rules.
|
||||
|
||||
## Review Process
|
||||
|
||||
1. Identify the review scope. For pending changes, inspect `git diff --stat`, `git diff`, and staged diff if relevant. For file-focused reviews, stay within the named files unless a referenced owner/contract must be read.
|
||||
2. Read code around the changed lines and the owning module. Do not review by isolated snippets when nearby ownership, labels, query inputs, or overlay structure decide correctness.
|
||||
3. Check user-visible regressions first: accessibility, broken interaction, auth/permission leaks, query/hydration errors, data loss, navigation mistakes, and impossible states.
|
||||
4. Then check maintainability and performance: ownership, effects, wrappers, memoization, bundle/waterfall risks, tests, and design-system drift.
|
||||
5. Report only actionable findings. Do not list speculative risks, style preferences, or broad refactors unless they are directly tied to a reproducible issue in scope.
|
||||
|
||||
## Severity
|
||||
|
||||
- **P0**: security/privacy/auth leak, data loss, production crash, inaccessible critical flow, or broken primary workflow.
|
||||
- **P1**: user-visible regression, hydration/SSR failure, invalid API/query contract, broken keyboard/focus behavior, or serious design-system/a11y violation.
|
||||
- **P2**: maintainability or performance issue likely to cause bugs, duplicated state, incorrect ownership, missing tests for risky behavior, or non-critical a11y issue.
|
||||
- **P3**: minor cleanup with clear value. Omit unless the user asked for a thorough audit.
|
||||
|
||||
## Output Format
|
||||
|
||||
Lead with findings, ordered by severity. Use this structure:
|
||||
|
||||
```markdown
|
||||
## Findings
|
||||
|
||||
- [P1] Short issue title
|
||||
File: `path/to/file.tsx:123`
|
||||
Why it matters and how to reproduce or reason about it.
|
||||
Suggested fix: concrete fix direction.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Question or assumption, if any.
|
||||
|
||||
## Summary
|
||||
|
||||
Brief secondary context. Mention tests not run or residual risk.
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- If there are no findings, say `No issues found.` and mention any test gaps or residual risk.
|
||||
- Always include file and line when available.
|
||||
- Keep findings concrete and reproducible.
|
||||
- Do not include praise sections by default.
|
||||
- Do not ask to apply fixes unless the user explicitly wants review plus implementation.
|
||||
[accessibility]: references/accessibility-ui.md
|
||||
[code-quality]: references/code-quality.md
|
||||
[component-architecture]: references/component-architecture.md
|
||||
[data-query]: references/data-query-contracts.md
|
||||
[dify-invariants]: references/dify-invariants.md
|
||||
[dify-ui]: references/dify-ui.md
|
||||
[performance]: references/performance.md
|
||||
[testing]: references/testing.md
|
||||
|
||||
@@ -46,14 +46,13 @@ When existing components already own interaction logic, prefer reusing or extend
|
||||
|
||||
Flag:
|
||||
|
||||
- `React.FC` / `FC`.
|
||||
- Default exports outside framework-required files.
|
||||
- Declaration or export rewrites made only for stylistic uniformity, without changing an owned behavior or contract.
|
||||
- Named `Props` types for trivial one-off props where inline typing is clearer.
|
||||
- Props named by UI implementation instead of domain/API role.
|
||||
- API data converted too early or under a generic name that breaks traceability.
|
||||
- Callers duplicating fallback checks that the lowest rendering component already handles.
|
||||
|
||||
Prefer top-level `function` declarations for components and module helpers. Use arrow functions for callbacks and local lambdas.
|
||||
Do not flag `FC`, `React.FC`, function declarations, arrow functions, named exports, or default exports by syntax alone. Report them only when the chosen form causes a concrete type, lifecycle, export, framework, or enforced package-contract defect.
|
||||
|
||||
## Effects
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Flag:
|
||||
- Re-declaring API DTOs in components.
|
||||
- Adding compatibility layers instead of migrating the pointed line and deleting the old layer.
|
||||
|
||||
Use `web/contract/*` as the API shape source of truth. Follow existing `{ params, query?, body? }` input shape.
|
||||
Backend Pydantic and OpenAPI schemas own API shape. Generated clients and schemas under `packages/contracts/generated/*` are authoritative at frontend boundaries and use the `{ params, query?, body? }` input shape.
|
||||
|
||||
## Queries
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ Flag:
|
||||
- Manual class strings that duplicate primitive variants.
|
||||
- `min-w-(--anchor-width)` on picker popups when it defeats viewport clamping.
|
||||
|
||||
Use the Figma radius mapping from `packages/dify-ui/AGENTS.md`; for example `--radius/sm` maps to `rounded-md`, and `--radius/md` maps to `rounded-lg`.
|
||||
Use the Figma radius mapping from `packages/dify-ui/README.md`; for example `--radius/sm` maps to `rounded-md`, and `--radius/md` maps to `rounded-lg`.
|
||||
|
||||
Use `!` only for a tightly scoped compatibility override after confirming the primitive API, data attributes, and selector structure cannot express the state.
|
||||
|
||||
|
||||
@@ -1,35 +1,16 @@
|
||||
---
|
||||
name: frontend-testing
|
||||
description: Write, update, or review Dify frontend tests using Vitest and Testing Library. Trigger for frontend specs, test coverage requests, regressions, testability, or testing strategy under web/ or packages/dify-ui/.
|
||||
description: Use when writing or changing Vitest or React Testing Library tests under `web/` or `packages/dify-ui/`, or when the user explicitly requests frontend test strategy, including evaluation of an existing strategy. Do not use for frontend code-review-only requests, general testability discussion, Python tests, or Cucumber/Playwright E2E.
|
||||
---
|
||||
|
||||
# Dify Frontend Testing
|
||||
# Frontend Testing
|
||||
|
||||
Use this skill for Vitest work under `web/` and `packages/dify-ui/`. Do not use it for Python tests or Cucumber/Playwright tests under `e2e/`.
|
||||
`web/docs/test.md` is the single policy owner. Read it before changing frontend tests; this skill adds no separate requirements.
|
||||
|
||||
## Required Source
|
||||
1. Identify the observable contract and regression risk.
|
||||
2. Choose the smallest boundary that includes the behavior owner.
|
||||
3. Establish the failing case first when practical, then implement one coherent scenario.
|
||||
4. Run the focused spec before the affected suite and relevant static checks.
|
||||
5. Report the behavior verified and any remaining browser, visual, or end-to-end risk.
|
||||
|
||||
Before writing, changing, or reviewing frontend tests, read `web/docs/test.md` completely. It is the single source of truth. This skill provides an execution checklist and must not redefine or extend that policy.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read the source, its behavior owner, nearby specs, and relevant public dependencies.
|
||||
1. Apply the canonical guide to decide whether a test is needed and choose its boundary.
|
||||
1. For a behavior change or bug fix, write or identify the failing scenario first when practical.
|
||||
1. Implement one coherent scenario at a time and run the focused spec before expanding scope.
|
||||
1. Finish with the affected suite and relevant repository checks.
|
||||
1. Report what behavior was verified and any risk that still requires browser, visual, or end-to-end validation.
|
||||
|
||||
When reviewing existing tests, recommend deleting low-value tests as readily as adding missing behavior coverage.
|
||||
|
||||
Run focused tests from the owning workspace:
|
||||
|
||||
```bash
|
||||
# web/
|
||||
vp test run path/to/spec-or-directory
|
||||
|
||||
# packages/dify-ui/
|
||||
vp test run --project unit src/path/to/spec
|
||||
```
|
||||
|
||||
Run Dify UI Storybook tests with `vp test --project storybook --run`. Run broader checks only after the focused behavior passes.
|
||||
Recommend deleting low-value tests as readily as adding missing behavior coverage. Use `web/docs/test.md` for policy and Web commands; use the `packages/dify-ui/README.md` Development section for Dify UI commands.
|
||||
|
||||
@@ -1,144 +1,41 @@
|
||||
---
|
||||
name: how-to-write-component
|
||||
description: Use when writing, refactoring, or reviewing React/TypeScript components in Dify web, especially decisions about component ownership, props/types, URL/query state, Jotai state, async state, generated API contracts, queries/mutations, overlays, effects, navigation, performance, and empty states.
|
||||
description: Use when implementing or refactoring React/TypeScript components and the task requires decisions about component ownership, feature boundaries, state, data flow, effects, or interaction ownership. Do not use for review-only requests, test-only work, copy-only edits, or styling-only changes.
|
||||
---
|
||||
|
||||
# How To Write A Component
|
||||
|
||||
Use this as the component decision guide for Dify web. Existing code is reference material, not automatic precedent; if touched code violates these rules, adapt it and fix equivalent patterns in the same feature branch.
|
||||
Use this skill to route component architecture decisions to its bundled references. Read only the references required by the current change.
|
||||
|
||||
## First Decisions
|
||||
|
||||
| Question | Default | Promote or extract only when |
|
||||
| Question | Default | Promote only when |
|
||||
| --- | --- | --- |
|
||||
| Where should code live? | Keep it local to the feature workflow, route, or owner. | Multiple verticals need the same stable primitive. |
|
||||
| How should route/tab folders be named? | Match the current route segment, tab name, or user-visible surface. | Keep a historical or broader parent only when it still owns multiple surfaces. |
|
||||
| Who owns state, data, and handlers? | The lowest component that uses them. | A parent coordinates shared loading, errors, empty UI, selection, submission, navigation, or one consistent snapshot. |
|
||||
| Should this become Jotai state? | Keep synchronous UI/form state in component or DOM state. | Siblings need one source of truth, the value drives atoms, or scoped workflow state must survive hidden/unmounted steps. |
|
||||
| Should URL state enter Jotai? | Let Next.js route params and `nuqs` own URL state and updates. | Query atoms or shared derived atoms need a read-only bridge hydrated at the route/surface boundary. |
|
||||
| Should this query/mutation become an atom? | Use TanStack Query hooks at the lowest owner. | It reads atom state, feeds derived atoms, or participates in shared Jotai workflow orchestration. |
|
||||
| Should this be a helper/wrapper? | Prefer direct readable code at the use site. | The name captures a stable domain rule or the wrapper owns real behavior, validation, state, error handling, or semantics. |
|
||||
| Where should a hotkey live? | Keep a single-owner hotkey constant in its component. | Multiple production files share one command, or the feature owns a real command registry with shared metadata and behavior. |
|
||||
| Is an Effect needed? | No. Derive during render or handle the user action in the event handler. | It synchronizes with an external system such as browser APIs, subscriptions, timers, analytics, or imperative DOM/non-React widgets. |
|
||||
| Where should code live? | In the product workflow, route, or feature owner. | Several verticals need the same stable contract. |
|
||||
| Who owns state and handlers? | The lowest visual owner that consumes them. | A parent coordinates one workflow or consistent snapshot. |
|
||||
| Should state enter Jotai? | Keep component and form state local. | Siblings need one source of truth or scoped workflow persistence. |
|
||||
| Who owns URL state? | Next.js route APIs and `nuqs`. | Atoms require a read-only route-identity bridge. |
|
||||
| Who owns remote state? | TanStack Query at the lowest consumer. | Atom state drives the query or shared derivations consume it. |
|
||||
| Is a wrapper needed? | Use the primitive or direct code. | The wrapper owns behavior, validation, state, or semantics. |
|
||||
| Is an Effect needed? | Derive during render or handle the user action. | A named external system must be synchronized. |
|
||||
|
||||
## Core Defaults
|
||||
## Topic Routing
|
||||
|
||||
- Search before adding UI, hooks, helpers, query utilities, or styling patterns. Reuse existing base components, feature components, hooks, utilities, and design styles when they fit.
|
||||
- Follow Dify's CSS-first Tailwind v4 contract from `packages/dify-ui/README.md` and `packages/dify-ui/AGENTS.md`. Prefer design-system tokens, utilities, and radius mappings over generic Tailwind choices.
|
||||
- Preserve visible keyboard focus states on the final focusable element. Prefer styled `@langgenius/dify-ui/*` controls when available, because components such as `Button` and form/control primitives carry the standard Dify UI `focus-visible` styling. Do not assume every Dify UI export provides visual focus styles: headless anatomy parts and direct Base UI re-exports such as dialog/popover/tooltip/drawer triggers usually only provide behavior and semantics. When using native `button` / `a`, custom trigger `render` props, clickable rows, icon buttons, menu-like items, or direct trigger parts, verify the rendered focusable element has a visible focus state. If it does not, add the standard Dify UI focus style: `outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid`. Do not hide outlines without an equivalent visible `focus-visible` indicator. Component-specific focus styles should follow an existing styled primitive pattern or a concrete design constraint, not a new ad hoc style.
|
||||
- Group feature code by workflow, route, or ownership area with route-aligned names: components, hooks, local types, query helpers, atoms, constants, tests, and small utilities should live near the code that changes with them.
|
||||
- For each feature module, keep a module-local `README.md` as a boundary note. Start with the module name, a brief one-sentence description, then split dependencies into `Internal Modules` and `External Modules` sections; keep both sections and write `None.` when one category is empty. `Internal Modules` lists modules inside the same overall feature using paths from that feature root, such as `shared/domain/runtime-status`; `External Modules` lists project modules outside the feature using paths from the web root without a `web/` prefix, such as `app/components/base/skeleton`. Omit npm packages, workspace package dependencies, and whitelisted plumbing modules. Do not copy caller-relative import paths into the README.
|
||||
- Module README whitelist: `@/service/client`, `@/next/*`.
|
||||
- Keep source/default selection, validation, dirty checks, and payload shaping close to the workflow that owns submit behavior. Do not hide flow-specific priority order, fallback behavior, or submit semantics in generic utilities.
|
||||
- Prefer direct conditionals for small branch-specific decisions, especially form source selection and request payload assembly.
|
||||
- Loading states for page sections, cards, lists, tables, forms, and drawers should be skeletons scoped to the content being loaded. Use spinners only for small inline busy indicators.
|
||||
- Component moves, module boundaries, props, types, or owner placement: read [`references/ownership.md`][ownership].
|
||||
- Jotai, form drafts, route identity, URL state, or persistence: read [`references/state.md`][state].
|
||||
- Generated contracts, nullable API data, Query, mutations, SSR, auth, or workspace state: read [`references/data.md`][data].
|
||||
- Hotkeys, focus, dialogs, menus, popovers, or other secondary surfaces: read [`references/interactions.md`][interactions] and the overlay guide it references when applicable.
|
||||
- Effects, navigation, memoization, preloading, or render cost: read [`references/runtime.md`][runtime].
|
||||
|
||||
## Layout And Ownership
|
||||
## Workflow
|
||||
|
||||
- State-heavy wizards, drawers, modals, and secondary workflows can be a small feature surface: an entry file, one feature-local state file when Jotai is actually needed, and shallow `ui/` owners that match real visual regions.
|
||||
- The entry file handles route integration, provider wiring, close behavior, and surface mounting. The composition owner handles high-level workflow branching. The closest visual owner handles section branching.
|
||||
- When a page or tab maps to a route segment, name its feature folder after that route/tab surface instead of a stale parent grouping. Remove misleading intermediate folders when only one surface remains.
|
||||
- When a tab folder grows into several independent sections or action areas, split the first level by product/visual owners. Keep the root for the entry component and cross-owner state, colocate tests with the owner folder, and put truly shared local UI under a specifically named `components/` file.
|
||||
- Repeated TanStack query calls in sibling components are acceptable when each component independently consumes the data; TanStack Query deduplicates and shares cache.
|
||||
- Pass stable domain identity across boundaries. Do not forward derived presentation state when the receiver can derive it from its own data source.
|
||||
- A component that owns a visual surface should also own data access, loading, empty, and error states for content rendered inside it unless a parent truly coordinates that state.
|
||||
- Avoid prop drilling. One pass-through layer is acceptable; repeated forwarding means ownership should move down or into feature-scoped Jotai UI state. Keep server/cache state in Query and API flow.
|
||||
- Do not replace prop drilling with one large view-model hook threaded through section props. Move each hook, query, derived value, and handler to the concrete section that consumes it.
|
||||
- Keep callbacks in a parent only for workflow coordination such as form submission, shared selection, batch behavior, or navigation. Otherwise let the child, menu, or row own the action.
|
||||
1. Identify the behavior owner and the public contract being changed.
|
||||
2. Read the nearby implementation, tests, and only the routed skill references.
|
||||
3. Implement one coherent vertical slice. Do not expand into equivalent patterns elsewhere unless the current contract cannot be completed without them.
|
||||
4. Verify observable behavior at the narrowest sufficient boundary, then run the checks documented by the owning package: `web/docs/test.md` or `web/docs/lint.md` for Web, and the `packages/dify-ui/README.md` Development section for Dify UI.
|
||||
|
||||
## Feature-Scoped Jotai
|
||||
|
||||
- A Jotai-backed feature has one feature-local state file for shared primitive atoms, query atoms, derived atoms, write-only actions, mutation atoms, submission orchestration, provider exports, and optional scope configuration.
|
||||
- Keep component-owned synchronous UI state local even inside Jotai features: dialog open flags, menus/popovers, confirmations, field drafts, and selected local options usually belong in component state.
|
||||
- Use uncontrolled `@langgenius/dify-ui/form` and `@langgenius/dify-ui/field` controls for edit/create forms whose fields are read only at submit time. Initialize query-backed defaults with `defaultValue` and keyed remounts.
|
||||
- Promote form state to atoms only when another component must react to in-progress values, a draft must survive unmount/remount in the scoped workflow, or multiple steps share the same editable draft before submit.
|
||||
- Treat `useParams`, route args, and `nuqs` query state as framework-owned state. When atom logic needs those values, hydrate primitive atoms at the route or surface boundary, such as with `useHydrateAtoms(..., { dangerouslyForceHydrate: true })`; keep URL updates in the route/query-state APIs instead of write atoms.
|
||||
- Within a route-owned feature, choose one source for route identity. If route params are bridged into feature atoms, use that bridge consistently for route-derived queries and actions instead of also threading the same route id through page, tab, and section props.
|
||||
- For async work tied to atom state, use `atomWithQuery` or `atomWithMutation`; write atoms should update only the inputs that drive those atoms. This applies to pure frontend async work as well as network requests, so do not hand-roll loading/error/in-flight state with `useState` or `useRef` for atom-orchestrated async behavior. For component-owned remote work, use `useQuery` or `useMutation` directly.
|
||||
- `jotai-tanstack-query` query atoms do not support TanStack Query tracked properties. A component that reads `useAtomValue(queryAtom)` subscribes to the whole query result, even if it only accesses `data`, `isLoading`, or `isError`. Export field-specific derived atoms and have components read the exact fields they render; use `selectAtom(queryAtom, result => result.field)` for query-result fields so unchanged selections do not notify subscribers. Keep direct `useAtomValue(queryAtom)` only when the component or hook genuinely needs the full observer result.
|
||||
- Row-local async state belongs to the row owner unless it participates in a shared Jotai workflow or needs atom-scoped reset semantics.
|
||||
- Leave query and mutation atoms unscoped so they keep shared QueryClient cache and invalidation behavior. Scope resettable primitives and explicit hydration tuples; scope a derived atom only when every dependency should be private to that surface.
|
||||
- For scoped primitives that are always hydrated by `ScopeProvider`, prefer `atomWithLazy<T>(() => { throw new Error(...) })` when consumers should see a non-null type.
|
||||
- Order state files by dependency graph: types/constants, primitives, query atoms, query-data derived atoms, business/readiness derived atoms, write actions, mutation atoms, submission orchestration, provider exports.
|
||||
- Name derived atoms as business facts and write atoms as user or workflow commands. Components should read or write the exact atom they need with `useAtomValue` or `useSetAtom`.
|
||||
- Menu/dialog `open` state usually stays local, but a scoped atom is acceptable when a composed menu plus secondary surface would otherwise pass confusing `open`/`onClose` props through unrelated layers. Scope that primitive with the surface instance so reset behavior stays local.
|
||||
- Keep independent dialog lifecycles separate. Avoid one discriminated "current action dialog" atom when dialogs have separate open state, loading guards, or reset behavior.
|
||||
|
||||
## Components, Props, And Types
|
||||
|
||||
- Type component signatures directly; do not use `FC` or `React.FC`.
|
||||
- Prefer `function` for top-level components and module helpers. Use arrow functions for local callbacks, handlers, and lambda-style APIs.
|
||||
- Prefer named exports. Use default exports only where the framework requires them, such as Next.js route files.
|
||||
- Avoid barrel files that only re-export secondary owners. `index.tsx` is acceptable for a route/tab entry component; import header controls, switches, sections, and row owners from their concrete owner files.
|
||||
- Type simple one-off props inline. Use a named `Props` type only when reused, exported, complex, or clearer.
|
||||
- Use API-generated or API-returned types at component boundaries. Keep small UI conversion helpers and one-off UI extensions beside the component that needs them.
|
||||
- Preserve domain value types for selection components. Do not widen enum, union, boolean, numeric, object, or nullable select/radio values to `string`; keep wrappers and option value carriers typed from their feature option collection.
|
||||
- Avoid `common.tsx` buckets for shared UI. Use a feature-local `components/` folder with concrete filenames that describe the shared role.
|
||||
- Do not create type aliases that only rename another type. Use aliases only for real UI concepts, refinements, or reusable local contracts.
|
||||
- Name values by their domain role and backend API contract, especially persistent IDs and route params. Normalize framework or route params at the boundary.
|
||||
- Put fallback and invariant checks in the lowest component that already handles that state. Do not extract helpers whose only behavior is hiding missing display data.
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
- Distinguish application commands from local keyboard semantics before choosing an API. Use `@tanstack/react-hotkeys` for application commands. Keep menu navigation, dialog Escape handling owned by a primitive, editor commands, and other widget-scoped ARIA interactions in their local component or primitive.
|
||||
- Use `useHotkey` or `useHotkeys` for registered commands. For a command intentionally owned by an existing `onKeyDown`, use `matchesKeyboardEvent` instead of hand-written `metaKey` / `ctrlKey` parsing or a second global listener.
|
||||
- Define a reusable string command with `satisfies Hotkey` and an object-form command with `satisfies RawHotkey`. Reserve `RegisterableHotkey` for API boundaries that intentionally accept either form. A one-time inline literal passed directly to TanStack is already type-checked; extract it when registration, display, metadata, or another production consumer needs the same source.
|
||||
- Keep registered hotkeys distinct from held keys and display-only accelerators. Use `IndividualKey` with `useKeyHold` for held-key interactions, and use an explicitly named `displayKey` for local widget accelerators that are not registered `Hotkey` values.
|
||||
- Keep registration and keycap/menu display derived from one canonical command. Do not maintain a hotkey string beside a separate `['Mod', ...]` display array.
|
||||
- Keep a single-owner command constant in its owning component. Create a feature-local `hotkeys.ts` only when multiple production files consume the same command. Keep a dedicated definitions/registry module when a feature owns a real command system with IDs, metadata, alternate bindings, and centralized registration. Tests do not count as another production owner, and file-name uniformity alone is not a reason to extract.
|
||||
- Make scope and availability explicit. Use `enabled` for business or surface lifecycle, `ignoreInputs` for whether input-like elements may trigger the command, and `target` when the command belongs to a concrete DOM subtree. Global application commands may use the document target; inline editors and composed overlays should prefer the actual editor or Base UI Popup ref when that owner is exposed.
|
||||
- Put a scoped ref on the real behavior owner. Do not add a wrapper DOM element solely to obtain a hotkey target. If a shared overlay convenience component hides the Popup ref, either rely on its modal lifecycle/focus boundary when that is sufficient or design the primitive API separately; do not create a fake owner at the call site.
|
||||
- Set `preventDefault` and `stopPropagation` according to the existing product behavior and browser interaction. Do not silently accept TanStack defaults when migrating from another listener if that changes typing, submission, or propagation semantics.
|
||||
- Test observable command behavior, disabled/input/target scope, and the shared registration/display contract at the owning feature boundary. Prefer partial mocks that retain TanStack formatting and matching behavior when a registration boundary must be isolated.
|
||||
|
||||
## Generated API And Nullable Data
|
||||
|
||||
- Treat generated contracts as authoritative at API, query, mutation, cache, and service boundaries. For enterprise APIs, use `packages/contracts/generated/enterprise/*`.
|
||||
- Do not hand-write DTO mirrors, widen generated fields/enums, or add parallel frontend enum/status layers unless they model product state not represented by the API.
|
||||
- Use generated enum objects and union types directly in props, comparisons, status logic, and i18n keys. Presentation-only tone maps should be keyed by generated enums.
|
||||
- Normalize or coerce only at real boundaries: user-entered forms, search, URL/query params, file names, DOM IDs, or legacy adapters.
|
||||
- Do not coerce nullable or optional API strings to `''` in query, derived model, or payload-building code. Keep `null` or `undefined` until the final boundary requiring a string.
|
||||
- Do not use `value || undefined` for mutation fields where `''` means "clear this value". Trim or normalize at the form boundary, then preserve intentional empty strings.
|
||||
- Prefer nullable-tolerant render props for API-returned rows. Narrow only where a real value is required, such as mutation params, route hrefs, select values, query input, or required React keys.
|
||||
- Build required values in the same branch that proves them, using `flatMap`, a local loop, or an early return. Avoid truthiness guards, `filter(Boolean)`, `filter(item => item.id)`, and `!` after filters.
|
||||
- Use conditional spreads or explicit pushes for conditional array items instead of `undefined` placeholders followed by narrowing filters.
|
||||
- Empty collection fallbacks are for not-yet-loaded query data or genuinely nullable collections at the owning render boundary, not for hiding required API fields.
|
||||
|
||||
## Queries And Mutations
|
||||
|
||||
- Keep `web/contract/*` as the API shape source of truth and follow the `{ params, query?, body? }` input shape.
|
||||
- Consume generated queries with `useQuery(consoleQuery.xxx.queryOptions(...))` or `useQuery(marketplaceQuery.xxx.queryOptions(...))`.
|
||||
- If a generated query input comes from an atom, including a route-identity bridge atom, keep the query in `atomWithQuery`; do not unwrap the atom in a component just to call `useQuery`.
|
||||
- Consume owner-local mutations with `useMutation(consoleQuery.xxx.mutationOptions(...))` or `useMutation(marketplaceQuery.xxx.mutationOptions(...))` when pending/error state is not consumed by feature atoms.
|
||||
- In `atomWithQuery`, `atomWithInfiniteQuery`, and `atomWithMutation`, return generated `queryOptions()`, `infiniteOptions()`, or `mutationOptions()` directly. Pass `enabled`, `retry`, `placeholderData`, `select`, and pagination options into the generated call instead of spreading options into a hand-built object.
|
||||
- For generated oRPC options with missing required input, branch the whole input with `input: condition ? validInput : skipToken` and `enabled: Boolean(condition)`. Never place `skipToken` inside a nested placeholder payload or coerce required IDs to `''`.
|
||||
- When prefetch and render use the same request, extract local query options or a query-options atom so `prefetchQuery` and `useQuery`/`atomWithQuery` share the exact options.
|
||||
- For custom query or mutation functions, wrap options with TanStack `queryOptions(...)` or `mutationOptions(...)`.
|
||||
- Do not extract generated `queryOptions(...)` into a helper solely to share input construction; extract only when prefetch/render must share exact options or the helper owns real domain behavior.
|
||||
- Avoid pass-through hooks and thin `web/service/use-*` wrappers that only rename generated options. Keep feature hooks for real orchestration, workflow state, or shared domain behavior.
|
||||
- Put shared cache behavior in `createTanstackQueryUtils(...experimental_defaults...)`. Component or atom callbacks may handle local toasts, closing dialogs, and navigation, but should not replace shared invalidation or patch shared server state locally.
|
||||
- For overlays that may open heavier secondary content, prefetch from the trigger/menu open event with `queryClient.prefetchQuery(queryOptions)` when `onOpenChange` is available. Do not mount hidden subscribers just to warm cache.
|
||||
- Do not use deprecated `useInvalid` or `useReset`.
|
||||
- Prefer `mutate(...)`; use `mutateAsync(...)` only when Promise semantics are required, and wrap awaited calls in `try/catch`.
|
||||
|
||||
## Boundaries And Overlays
|
||||
|
||||
- Use the first level below a page or tab to organize independent page sections when it adds structure or the root folder becomes noisy. This layer is layout/semantic first, not automatically the data owner.
|
||||
- Treat component names, semantic roles, and user- or design-marked visual regions as boundary constraints. Keep adjacent UI as a sibling owner or introduce a correctly named broader owner.
|
||||
- Keep cohesive forms, menu bodies, and one-off helpers local unless they need their own state, reuse, or semantic boundary.
|
||||
- Separate hidden secondary surfaces from the trigger's main flow. For dialogs, dropdowns, popovers, and similar branches, extract a small local component when hidden content would obscure the parent.
|
||||
- Preserve composability by separating behavior ownership from placement ownership: an action can own trigger/open/menu content while the caller owns slots, offsets, and alignment.
|
||||
- When a dialog, dropdown, or popover accepts controlled `open`, mount it unconditionally unless unmounting is required for performance or reset semantics. Use keyed scope or local state reset instead of `{open && <Surface />}` wrappers.
|
||||
- When opening a dialog from a menu item, keep the menu and dialog as sibling surfaces. Let the menu command open the dialog, and mount the dialog outside menu popup content.
|
||||
- For dialogs and alert dialogs, keep the root responsible for `open` wiring and put query/mutation hooks inside the content component when work should mount only after the overlay opens.
|
||||
- Prefer uncontrolled overlay roots when the library can own open state. Use `onOpenChange` for side effects and CSS/data selectors for open-state styling.
|
||||
- Avoid wrapper DOM unless it provides layout, semantics, accessibility, state ownership, or library integration. Avoid shallow wrappers, hook-to-props adapters, layout-only render props, children pass-through wrappers, and prop renaming unless they add real behavior or a real boundary.
|
||||
|
||||
## Effects, Navigation, And Performance
|
||||
|
||||
- Use Effects only to synchronize with external systems. Do not use Effects to transform props/state for rendering, handle user actions, copy state, reset state from props, or fetch data.
|
||||
- For forms initialized from query data, prefer keyed remounts or surface-entry atom hydration over Effects that copy query data into form state.
|
||||
- Prefer framework data APIs or TanStack Query for data fetching.
|
||||
- Prefer `Link` for normal navigation. Use router APIs only for command-flow side effects such as mutation success, guarded redirects, or form submission.
|
||||
- Before using `memo`, move changing state down to the smallest component that uses it. If state must wrap stable content, lift the stable content up and pass it as `children`.
|
||||
- Avoid `memo`, `useMemo`, and `useCallback` unless there is a clear performance reason.
|
||||
[data]: references/data.md
|
||||
[interactions]: references/interactions.md
|
||||
[ownership]: references/ownership.md
|
||||
[runtime]: references/runtime.md
|
||||
[state]: references/state.md
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Component Data And Queries
|
||||
|
||||
Read this document when a component consumes generated contracts, nullable API values, TanStack Query, mutations, prefetching, authentication, or workspace state.
|
||||
|
||||
## Generated Contracts
|
||||
|
||||
- Treat generated contracts as authoritative at API, query, mutation, cache, and service boundaries. Enterprise APIs use `packages/contracts/generated/enterprise/*`.
|
||||
- Backend Pydantic and OpenAPI schemas own API shape. Follow the generated `{ params, query?, body? }` input shape; when it is wrong, fix the backend schema and regenerate `packages/contracts/generated/*`.
|
||||
- Do not hand-write DTO mirrors, widen generated fields or enums, edit generated output, or add a parallel frontend status layer unless it models product state absent from the API.
|
||||
- Check deprecated markers, schema shape, and the actual consumer before assuming that a generated operation is ready to use.
|
||||
- Normalize only at real boundaries such as user input, search, URL params, filenames, DOM IDs, or a required legacy adapter.
|
||||
- Preserve `null`, `undefined`, and intentional empty strings until the final boundary. Do not use `value || undefined` when an empty string means clearing a field.
|
||||
- Build required values in the branch that proves them. Avoid `filter(Boolean)`, truthiness filters, non-null assertions after filters, and placeholder values used only to satisfy types.
|
||||
|
||||
## Queries
|
||||
|
||||
- Use generated options directly with `useQuery(consoleQuery.xxx.queryOptions(...))`, `marketplaceQuery`, or the equivalent generated client.
|
||||
- If query input comes from atom state, keep it in `atomWithQuery`; do not unwrap the atom in a component solely to call `useQuery`.
|
||||
- For missing required input, branch the whole generated input with `skipToken`. Add `enabled` only for an independent execution condition; do not put `skipToken` inside a placeholder payload or coerce IDs to empty strings.
|
||||
- Return generated `queryOptions()`, `infiniteOptions()`, or `mutationOptions()` directly from TanStack Query atoms. Pass supported options into the generated call instead of spreading into a parallel object.
|
||||
- Share the exact options between prefetch and render when they represent the same request. Do not extract option helpers merely to reuse input construction.
|
||||
- Avoid pass-through service hooks that only rename generated options. Keep feature hooks for actual orchestration or shared domain behavior.
|
||||
|
||||
## Mutations And Cache
|
||||
|
||||
- Use generated `mutationOptions()` directly for owner-local mutations.
|
||||
- Put shared invalidation, retries, and cache behavior in `createTanstackQueryUtils(...experimental_defaults...)`. Local callbacks may own toast, close, and navigation effects but must not replace shared cache policy.
|
||||
- Prefer `mutate(...)`. Use `mutateAsync(...)` only when Promise composition is required, and catch awaited failures.
|
||||
- Preserve intentional empty values and current list/detail ownership when updating data. Do not add optimistic updates without a verified owner contract.
|
||||
|
||||
## Prefetch And Hidden Surfaces
|
||||
|
||||
- Prefetch expensive secondary content from the trigger or menu-open event when it benefits the visible path. Do not mount hidden subscribers solely to warm the cache.
|
||||
- `prefetchQuery` is cache warmup, not an authorization or availability gate. Use a hard fetch boundary when the server must decide whether rendering may proceed.
|
||||
|
||||
## SSR, Authentication, And Workspace
|
||||
|
||||
- Static configuration owns path-invariant routing. Request-dependent authentication, setup, role, and tenant decisions belong to SSR or runtime decision boundaries.
|
||||
- Distinguish soft SSR cache warming from authoritative decisions. Prefetched or placeholder data must not grant access or represent successful availability.
|
||||
- Never reuse tenant-scoped state after switching workspaces. Discard it at the switch boundary or isolate it by workspace identity.
|
||||
- Do not make product or authorization decisions from bootstrap defaults. Wait for authoritative data, or render an explicit loading or error state.
|
||||
- Keep loading and Suspense behavior inside the feature that owns the request. Do not add fake global data merely to bypass that boundary.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Component Interactions And Overlays
|
||||
|
||||
Read this document when a change involves application hotkeys, focus, dialogs, menus, popovers, or other secondary surfaces. Overlay primitive selection and layering are owned by the [overlay guide].
|
||||
|
||||
## Focus And Semantics
|
||||
|
||||
- Preserve a visible focus indicator on the final focusable element. Styled Dify UI controls usually provide it; headless anatomy parts and direct trigger exports may not.
|
||||
- Native buttons, links, custom trigger renderers, clickable rows, icon controls, and menu-like items must retain their correct native semantics and accessible name.
|
||||
- Do not hide an outline without an equivalent visible `focus-visible` treatment. Follow an existing Dify UI pattern rather than inventing a call-site style.
|
||||
|
||||
## Keyboard Commands
|
||||
|
||||
- Distinguish application commands from widget-local keyboard semantics. Use `@tanstack/react-hotkeys` for application commands; keep menu navigation, dialog Escape handling, editor behavior, and ARIA widget keys in their local primitive or owner.
|
||||
- Use `useHotkey` or `useHotkeys` for registered commands. When an existing `onKeyDown` intentionally owns the command, use `matchesKeyboardEvent` rather than duplicating modifier parsing or adding another global listener.
|
||||
- Keep registration and keycap or menu display derived from one canonical command. Distinguish registered commands, held keys, and display-only accelerators.
|
||||
- Keep a single-owner command beside its component. Create a feature-local hotkey module only when several production files share it; tests alone do not justify extraction.
|
||||
- Make availability and scope explicit with `enabled`, `ignoreInputs`, and `target`. Put a target ref on the actual behavior owner rather than creating wrapper DOM solely for hotkey scope.
|
||||
- Preserve existing `preventDefault` and propagation behavior when migrating command APIs.
|
||||
- Test observable command behavior, disabled and input scope, target scope, and the registration/display contract at the owning feature boundary.
|
||||
|
||||
## Secondary Surfaces
|
||||
|
||||
- Follow `web/docs/overlay.md` for primitive choice. Dify UI primitives are the default, with package-approved Web wrappers such as `Infotip` where the overlay guide allows them.
|
||||
- Separate behavior ownership from placement ownership: the action may own trigger, open state, and menu content while the caller owns slots, offsets, and alignment.
|
||||
- Keep menu and dialog surfaces as siblings when a menu command opens a dialog. Mount the dialog outside popup content.
|
||||
- Mount controlled overlays unconditionally unless unmounting is required for performance or reset semantics. Prefer keyed or owner-local reset over conditional wrappers.
|
||||
- Put query and mutation work inside dialog or alert-dialog content when it should mount only after opening.
|
||||
- Prefer uncontrolled roots when the primitive can own open state. Use controlled state only for business coordination, analytics, cleanup, or explicit reset behavior.
|
||||
- Do not add manual portals or call-site z-index escalation. Fix ownership and stacking structure at the shared boundary.
|
||||
|
||||
[overlay guide]: ../../../../web/docs/overlay.md
|
||||
@@ -0,0 +1,39 @@
|
||||
# Component Ownership And Modules
|
||||
|
||||
Read this document when adding, moving, splitting, or refactoring React components or feature modules.
|
||||
|
||||
## Vertical Modules
|
||||
|
||||
- Organize code by product workflow, route, or behavior owner. Keep components, hooks, local types, atoms, query helpers, tests, and small utilities beside the code that changes with them.
|
||||
- Name page and tab folders after the current route, tab, or user-visible surface. Do not preserve stale parent groupings that no longer own multiple surfaces.
|
||||
- Split a growing page or tab by product or visual owners. Keep the feature root for its public entrypoint and genuinely cross-owner coordination.
|
||||
- Import other features only through explicit public entrypoints. Avoid barrels that merely re-export secondary owners.
|
||||
- Promote code outside a feature only when multiple verticals use the same stable contract. Possible future reuse is not sufficient.
|
||||
|
||||
## Component Ownership
|
||||
|
||||
- Put state, data access, loading, empty, error, and handlers in the lowest visual owner that uses them.
|
||||
- Keep coordination in a parent only when it needs one consistent snapshot or coordinates submission, shared selection, batch behavior, navigation, or cross-section loading and errors.
|
||||
- Repeated TanStack Query calls in siblings are acceptable when each sibling independently consumes the data; the cache already deduplicates requests.
|
||||
- Pass stable domain identity across boundaries. Do not pass raw server data together with separately derived flags for the same concept.
|
||||
- One pass-through prop layer is acceptable. Repeated forwarding means ownership should move closer to the consumer or into feature-scoped shared state.
|
||||
- Do not replace prop drilling with one large view-model hook. Move each query, derived value, and handler to the concrete owner that consumes it.
|
||||
- Keep source selection, defaults, validation, dirty checks, and payload shaping beside the workflow that owns submission.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- State-heavy wizards, drawers, modals, and secondary workflows can form a small vertical surface with an entrypoint, optional feature-local state, and shallow owners matching real visual regions.
|
||||
- The entrypoint owns route integration, provider wiring, close behavior, and mounting. Composition owners handle workflow branches; the closest visual owner handles section branches.
|
||||
- Separate hidden dialogs, dropdowns, and popovers into small local owners when their content obscures the parent flow.
|
||||
- Keep cohesive forms, menu bodies, and one-off helpers local unless they have their own state, reuse, or semantic boundary.
|
||||
- Avoid wrapper components and wrapper DOM that only rename props, pass children through, or hide the real primitive. A wrapper must own behavior, validation, state, accessibility, layout, or library integration.
|
||||
- Loading states for page sections, cards, lists, tables, forms, and drawers should use skeletons scoped to the loaded content. Reserve spinners for small inline busy indicators.
|
||||
|
||||
## Components And Types
|
||||
|
||||
- Choose component declaration and export forms from the actual component contract, framework requirements, and enforced package rules. Existing style is context, not authority; do not rewrite unaffected code solely to normalize `FC`, `function`, arrow-function, named-export, or default-export forms.
|
||||
- Type simple one-off props inline. Name a `Props` type when it is reused, exported, complex, or materially clearer.
|
||||
- Use API-generated or API-returned types at component boundaries. Keep one-off UI refinements and conversions beside their owner.
|
||||
- Preserve domain value types for selections. Do not widen enums, unions, booleans, numbers, objects, or nullable values to `string` before a real boundary requires it.
|
||||
- Avoid generic `common.tsx` buckets and aliases that only rename another type. Name files, values, and public types after their domain role.
|
||||
- Put fallback and invariant checks in the lowest component that already renders that state. Do not extract helpers whose only purpose is hiding missing display data.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Component Effects, Navigation, And Runtime Cost
|
||||
|
||||
Read this document when a change introduces Effects, navigation side effects, memoization, preloading, or render-cost optimizations.
|
||||
|
||||
## Effects
|
||||
|
||||
- Use Effects only to synchronize with a named external system such as a browser API, subscription, timer, analytics integration, non-React widget, or imperative DOM API.
|
||||
- Do not use Effects to transform render state, handle user actions, copy query data, reset state from props, or fetch data owned by framework APIs or TanStack Query.
|
||||
- Initialize query-backed forms with keyed remounts or surface-entry hydration instead of copying data through Effects.
|
||||
|
||||
## Navigation
|
||||
|
||||
- Use `Link` for ordinary navigation.
|
||||
- Use router APIs for command-flow side effects such as mutation success, guarded redirects, or form submission.
|
||||
- Keep shareable navigation state in the URL rather than hidden component state.
|
||||
|
||||
## Runtime Cost
|
||||
|
||||
- Move changing state to the smallest consumer before considering memoization. Stable parent content can be lifted and passed as children.
|
||||
- Avoid `memo`, `useMemo`, and `useCallback` unless identity or computation has a demonstrated consumer or measurable cost.
|
||||
- Start independent remote work together and await it near the branch that consumes it. Avoid introducing request waterfalls.
|
||||
- Load heavy optional surfaces on demand when they sit behind a dialog, tab, command, or feature activation.
|
||||
- Use narrow selectors or field-level atoms for broad stores and subscriptions. Do not optimize simple primitive expressions merely for stylistic consistency.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Component State And URL Ownership
|
||||
|
||||
Read this document when a change involves Jotai, form drafts, route identity, shared client state, or local persistence.
|
||||
|
||||
## Choose The Owner
|
||||
|
||||
- Keep synchronous state local when one component owns it: dialog and menu state, confirmations, field drafts, and local selections usually belong to the component or DOM.
|
||||
- Use feature-scoped Jotai when siblings need one source of truth, values drive other atoms, or a scoped workflow must preserve state across hidden or unmounted steps.
|
||||
- Keep server and cache state in TanStack Query. Use existing feature stores for complex, high-frequency interaction state such as workflow canvas drag, resize, and runtime panels.
|
||||
- Use feature-owned storage only for low-frequency client preferences, dismissed notices, and UI defaults. Live application state does not belong in local storage.
|
||||
|
||||
## Forms
|
||||
|
||||
- Prefer uncontrolled Dify UI form and field controls when values are only read at submit time. Initialize query-backed defaults with `defaultValue` and keyed remounts.
|
||||
- Promote form values to atoms only when another owner reacts to in-progress values, the draft must survive scoped unmounting, or several workflow steps edit the same draft.
|
||||
- Keep validation, source priority, fallback behavior, dirty checks, and payload assembly in the workflow that owns submission.
|
||||
|
||||
## Route And URL State
|
||||
|
||||
- Treat `useParams`, route arguments, and `nuqs` as the owners of URL identity and updates.
|
||||
- Hydrate a primitive atom at the route or surface boundary only when query atoms or shared derived atoms require route identity. Keep URL writes in route and query-state APIs.
|
||||
- Within one route-owned feature, choose one route-identity source. Do not hydrate route identity into atoms while also threading the same ID through multiple component layers.
|
||||
- Put shareable filters, tabs, pagination, and search state in the URL. Keep one-shot navigation signals and transient UI state out of persistent subscriptions.
|
||||
|
||||
## Jotai And Query
|
||||
|
||||
- A Jotai-backed feature may keep one feature-local state module ordered by dependency: types and constants, primitives, query atoms, query-data derivations, business facts, commands, mutations, submission orchestration, and provider exports.
|
||||
- Use `atomWithQuery` or `atomWithMutation` for async work driven by atom state. Do not hand-roll loading, error, or in-flight state for atom-orchestrated work.
|
||||
- Use field-specific derived atoms for query results. `jotai-tanstack-query` does not provide TanStack Query tracked properties, so reading a whole query atom subscribes to the entire observer result.
|
||||
- Leave query and mutation atoms unscoped so they retain the shared QueryClient cache. Scope resettable primitives and hydration tuples; scope a derived atom only when all dependencies should be private to the surface.
|
||||
- Use non-null lazy primitives for values always hydrated by a scope provider. Name derived atoms as business facts and write atoms as user or workflow commands.
|
||||
- Keep independent dialog lifecycles separate. A scoped open-state atom is acceptable only when composed sibling surfaces would otherwise pass confusing lifecycle props through unrelated owners.
|
||||
|
||||
## Persistence
|
||||
|
||||
- Use feature-owned storage modules built on `createLocalStorageState`; callers should not scatter direct storage access or raw keys.
|
||||
- Persist high-frequency interaction state only on commit or after updates settle.
|
||||
- Do not add ad hoc global event listeners for shared state. Centralize subscriptions through the owning atom, store, or subscription hook.
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
name: karpathy-guidelines
|
||||
description: Lightweight coding guardrails for making focused, simple, and verifiable changes in this repo. Use for all coding work.
|
||||
---
|
||||
|
||||
# Karpathy Guidelines
|
||||
|
||||
Use this skill whenever you touch code in this repository.
|
||||
|
||||
## Principles
|
||||
|
||||
- Keep the change small and directly tied to the user request.
|
||||
- Prefer the simplest implementation that fits the existing codebase.
|
||||
- Read the nearby code first, then match its patterns.
|
||||
- Avoid unrelated refactors, broad rewrites, or style churn.
|
||||
- Preserve existing behavior unless the user explicitly asked to change it.
|
||||
- Treat regressions as a signal to narrow the change, not to add workaround layers.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect the current implementation and tests around the change.
|
||||
2. Make the smallest coherent edit.
|
||||
3. Add or update focused tests when the behavior changes or the risk is non-trivial.
|
||||
4. Run the narrowest relevant verification first.
|
||||
5. Report exactly what was verified and anything left unverified.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- Does this change solve the stated problem without expanding scope?
|
||||
- Did it preserve existing route/component/data-flow semantics?
|
||||
- Are new abstractions justified by real complexity?
|
||||
- Are tests focused on the behavior that could regress?
|
||||
- Are unrelated files and generated artifacts left alone?
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/component-refactoring
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/karpathy-guidelines
|
||||
@@ -250,5 +250,22 @@
|
||||
# Frontend - Workspace
|
||||
/web/app/components/header/account-dropdown/workplace-selector/ @iamjoel @zxhlyh
|
||||
|
||||
# Frontend - App Shell and Console Bootstrap
|
||||
/web/app/layout.tsx @iamjoel @lyzno1
|
||||
/web/app/error.tsx @iamjoel @lyzno1
|
||||
/web/app/(commonLayout)/layout.tsx @iamjoel @lyzno1
|
||||
/web/app/(commonLayout)/providers.tsx @iamjoel @lyzno1
|
||||
/web/app/(commonLayout)/hydration-boundary.tsx @iamjoel @lyzno1
|
||||
/web/app/(commonLayout)/profile-bootstrap-gate.tsx @iamjoel @lyzno1
|
||||
/web/app/(commonLayout)/error.tsx @iamjoel @lyzno1
|
||||
/web/app/account/(commonLayout)/layout.tsx @iamjoel @lyzno1
|
||||
/web/app/components/main-nav/* @iamjoel @lyzno1
|
||||
/web/app/components/main-nav/components/* @iamjoel @lyzno1
|
||||
/web/context/query-client.tsx @iamjoel @lyzno1
|
||||
/web/context/query-client-server.ts @iamjoel @lyzno1
|
||||
/web/proxy.ts @iamjoel @lyzno1
|
||||
/web/app/auth/refresh/route.ts @iamjoel @lyzno1
|
||||
/web/service/server.ts @iamjoel @lyzno1
|
||||
|
||||
# Docker
|
||||
/docker/* @laipz8200
|
||||
|
||||
@@ -29,13 +29,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -88,13 +88,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -139,13 +139,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
run: echo "autofix.ci updates pull request branches, not merge group refs."
|
||||
|
||||
- if: github.event_name != 'merge_group'
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Check Docker Compose inputs
|
||||
if: github.event_name != 'merge_group'
|
||||
@@ -84,12 +84,12 @@ jobs:
|
||||
dify-agent/pyproject.toml
|
||||
dify-agent/uv.lock
|
||||
- if: github.event_name != 'merge_group'
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- if: github.event_name != 'merge_group'
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
|
||||
- name: Generate Docker Compose
|
||||
if: github.event_name != 'merge_group' && steps.docker-compose-changes.outputs.any_changed == 'true'
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USER }}
|
||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USER }}
|
||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
ws2_app_id: ${{ steps.out.outputs.DIFY_E2E_WS2_APP_ID }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
@@ -351,7 +351,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
working-directory: ./cli
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
dify_tag: ${{ steps.resolve.outputs.dify_tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
DIFY_TAG: ${{ needs.validate.outputs.dify_tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout cli ref
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
@@ -63,13 +63,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Deploy Knowledge
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Push API & Web"]
|
||||
branches:
|
||||
- "deploy/konwledge"
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: depot-ubuntu-24.04
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_branch == 'deploy/konwledge'
|
||||
steps:
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
||||
with:
|
||||
host: ${{ secrets.SSH_NEW_RAG_HOST }}
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }}
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Require cherry-pick provenance
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
|
||||
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
|
||||
with:
|
||||
sync-labels: true
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
migration-changed: ${{ steps.changes.outputs.migration }}
|
||||
sandbox-runtime-changed: ${{ steps.changes.outputs.sandbox-runtime }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: changes
|
||||
with:
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
outputs:
|
||||
external-e2e-changed: ${{ steps.changes.outputs.external_e2e }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: changes
|
||||
with:
|
||||
|
||||
@@ -17,12 +17,12 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python & UV
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ jobs:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
|
||||
steps:
|
||||
- name: Checkout default branch (trusted code)
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup Python & UV
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python & UV
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
- name: Setup UV and Python
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: false
|
||||
python-version: "3.12"
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -186,7 +186,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
working-directory: sdks/nodejs-client
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
|
||||
- name: Run Claude Code for Translation Sync
|
||||
if: steps.context.outputs.CHANGED_FILES != ''
|
||||
uses: anthropics/claude-code-action@af0559ee4f514d1ef21826982bed13f7edc3c35e # v1.0.178
|
||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1.0.183
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
remove_tool_cache: true
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
remove_tool_cache: true
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-web
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -1,46 +1,8 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project Overview
|
||||
Dify is an open-source platform for building LLM applications, agentic workflows, and RAG pipelines. This monorepo contains the backend API (`api/`), frontend application (`web/`), deployment assets (`docker/`), standalone agent backend (`dify-agent/`), CLI (`cli/`), and end-to-end suite (`e2e/`). Follow the nearest scoped `AGENTS.md` for the files being changed.
|
||||
|
||||
Dify is an open-source platform for developing LLM applications with an intuitive interface combining agentic AI workflows, RAG pipelines, agent capabilities, and model management.
|
||||
## Repository Gotchas
|
||||
|
||||
The codebase is split into:
|
||||
|
||||
- **Backend API** (`/api`): Python Flask application organized with Domain-Driven Design
|
||||
- **Frontend Web** (`/web`): Next.js application using TypeScript and React
|
||||
- **Docker deployment** (`/docker`): Containerized deployment configurations
|
||||
- **Dify Agent Backend** (`/dify-agent`): Backend services for managing and executing agent
|
||||
|
||||
## Backend Workflow
|
||||
|
||||
- Read `api/AGENTS.md` for details
|
||||
- Run backend CLI commands through `uv run --project api <command>`.
|
||||
- Integration tests are CI-only and are not expected to run in the local environment.
|
||||
|
||||
## Frontend Workflow
|
||||
|
||||
- Read `web/AGENTS.md` for details
|
||||
|
||||
## Testing & Quality Practices
|
||||
|
||||
- Follow TDD: red → green → refactor.
|
||||
- Use `pytest` for backend tests with Arrange-Act-Assert structure.
|
||||
- Enforce strong typing; avoid `Any` and prefer explicit type annotations.
|
||||
- Write self-documenting code; only add comments that explain intent.
|
||||
|
||||
## Language Style
|
||||
|
||||
- **Python**: Keep type hints on functions and attributes, and implement relevant special methods (e.g., `__repr__`, `__str__`). Prefer `TypedDict` over `dict` or `Mapping` for type safety and better code documentation.
|
||||
- **TypeScript**: Use the strict config, run `pnpm check` for formatting, Oxlint, ESLint non-code checks, and type checking, and avoid `any` types.
|
||||
|
||||
## General Practices
|
||||
|
||||
- Prefer editing existing files; add new documentation only when requested.
|
||||
- Inject dependencies through constructors and preserve clean architecture boundaries.
|
||||
- Handle errors with domain-specific exceptions at the correct layer.
|
||||
|
||||
## Project Conventions
|
||||
|
||||
- Backend architecture adheres to DDD and Clean Architecture principles.
|
||||
- Async work runs through Celery with Redis as the broker.
|
||||
- Frontend user-facing strings must use `web/i18n/en-US/`; avoid hardcoded text.
|
||||
- Run backend commands through `uv run --project api <command>`.
|
||||
- Backend integration tests are CI-only and are not expected to run locally.
|
||||
|
||||
+3
-1
@@ -666,6 +666,7 @@ PLUGIN_REMOTE_INSTALL_PORT=5003
|
||||
PLUGIN_REMOTE_INSTALL_HOST=localhost
|
||||
PLUGIN_MAX_PACKAGE_SIZE=15728640
|
||||
PLUGIN_MODEL_SCHEMA_CACHE_TTL=3600
|
||||
PLUGIN_MODEL_PROVIDERS_CACHE_ENABLED=true
|
||||
PLUGIN_MODEL_PROVIDERS_CACHE_TTL=86400
|
||||
# Comma-separated marketplace plugin IDs whose latest versions are installed for newly registered users.
|
||||
# Example: langgenius/openai,langgenius/gemini
|
||||
@@ -677,6 +678,8 @@ INNER_API_KEY_FOR_PLUGIN=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y
|
||||
|
||||
# Dify Agent backend
|
||||
AGENT_BACKEND_BASE_URL=http://localhost:5050
|
||||
# Bearer token sent to the Agent backend /runs API. Must match DIFY_AGENT_API_TOKEN on the server side.
|
||||
AGENT_BACKEND_API_TOKEN=dify-agent-run-token-for-dev-only
|
||||
AGENT_BACKEND_STREAM_READ_TIMEOUT_SECONDS=30
|
||||
AGENT_BACKEND_STREAM_MAX_RECONNECTS=3
|
||||
AGENT_BACKEND_RUN_TIMEOUT_SECONDS=1200
|
||||
@@ -729,7 +732,6 @@ OTEL_MAX_EXPORT_BATCH_SIZE=512
|
||||
OTEL_METRIC_EXPORT_INTERVAL=60000
|
||||
OTEL_BATCH_EXPORT_TIMEOUT=10000
|
||||
OTEL_METRIC_EXPORT_TIMEOUT=30000
|
||||
|
||||
# Prevent Clickjacking
|
||||
ALLOW_EMBED=false
|
||||
|
||||
|
||||
+15
-211
@@ -1,221 +1,25 @@
|
||||
# API Agent Guide
|
||||
|
||||
## Notes for Agent (must-check)
|
||||
Read surrounding module, class, and function docstrings plus non-obvious comments before changing backend behavior. They are local contracts; update them only when their owned behavior changes, and keep them aligned with the current code.
|
||||
|
||||
Before changing any backend code under `api/`, you MUST read the surrounding docstrings and comments. These notes contain required context (invariants, edge cases, trade-offs) and are treated as part of the spec.
|
||||
## Commands
|
||||
|
||||
Look for:
|
||||
Run backend checks from the repository root:
|
||||
|
||||
- The module (file) docstring at the top of a source code file
|
||||
- Docstrings on classes and functions/methods
|
||||
- Paragraph/block comments for non-obvious logic
|
||||
|
||||
### What to write where
|
||||
|
||||
- Keep notes scoped: module notes cover module-wide context, class notes cover class-wide context, function/method notes cover behavioural contracts, and paragraph/block comments cover local “why”. Avoid duplicating the same content across scopes unless repetition prevents misuse.
|
||||
- **Module (file) docstring**: purpose, boundaries, key invariants, and “gotchas” that a new reader must know before editing.
|
||||
- Include cross-links to the key collaborators (modules/services) when discovery is otherwise hard.
|
||||
- Prefer stable facts (invariants, contracts) over ephemeral “today we…” notes.
|
||||
- **Class docstring**: responsibility, lifecycle, invariants, and how it should be used (or not used).
|
||||
- If the class is intentionally stateful, note what state exists and what methods mutate it.
|
||||
- If concurrency/async assumptions matter, state them explicitly.
|
||||
- **Function/method docstring**: behavioural contract.
|
||||
- Document arguments, return shape, side effects (DB writes, external I/O, task dispatch), and raised domain exceptions.
|
||||
- Add examples only when they prevent misuse.
|
||||
- **Paragraph/block comments**: explain *why* (trade-offs, historical constraints, surprising edge cases), not what the code already states.
|
||||
- Keep comments adjacent to the logic they justify; delete or rewrite comments that no longer match reality.
|
||||
|
||||
### Rules (must follow)
|
||||
|
||||
In this section, “notes” means module/class/function docstrings plus any relevant paragraph/block comments.
|
||||
|
||||
- **Before working**
|
||||
- Read the notes in the area you’ll touch; treat them as part of the spec.
|
||||
- If a docstring or comment conflicts with the current code, treat the **code as the single source of truth** and update the docstring or comment to match reality.
|
||||
- If important intent/invariants/edge cases are missing, add them in the closest docstring or comment (module for overall scope, function for behaviour).
|
||||
- **During working**
|
||||
- Keep the notes in sync as you discover constraints, make decisions, or change approach.
|
||||
- If you move/rename responsibilities across modules/classes, update the affected docstrings and comments so readers can still find the “why” and the invariants.
|
||||
- Record non-obvious edge cases, trade-offs, and the test/verification plan in the nearest docstring or comment that will stay correct.
|
||||
- Keep the notes **coherent**: integrate new findings into the relevant docstrings and comments; avoid append-only “recent fix” / changelog-style additions.
|
||||
- **When finishing**
|
||||
- Update the notes to reflect what changed, why, and any new edge cases/tests.
|
||||
- Remove or rewrite any comments that could be mistaken as current guidance but no longer apply.
|
||||
- Keep docstrings and comments concise and accurate; they are meant to prevent repeated rediscovery.
|
||||
|
||||
## Coding Style
|
||||
|
||||
This is the default standard for backend code in this repo. Follow it for new code and use it as the checklist when reviewing changes.
|
||||
|
||||
### Linting & Formatting
|
||||
|
||||
- Use Ruff for formatting and linting (follow `.ruff.toml`).
|
||||
- Keep each line under 120 characters (including spaces).
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- Use `snake_case` for variables and functions.
|
||||
- Use `PascalCase` for classes.
|
||||
- Use `UPPER_CASE` for constants.
|
||||
|
||||
### Typing & Class Layout
|
||||
|
||||
- Code should usually include type annotations that match the repo’s current Python version (avoid untyped public APIs and “mystery” values).
|
||||
- Prefer modern typing forms (e.g. `list[str]`, `dict[str, int]`) and avoid `Any` unless there’s a strong reason.
|
||||
- For dictionary-like data with known keys and value types, prefer `TypedDict` over `dict[...]` or `Mapping[...]`.
|
||||
- For optional keys in typed payloads, use `NotRequired[...]` (or `total=False` when most fields are optional).
|
||||
- Keep `dict[...]` / `Mapping[...]` for truly dynamic key spaces where the key set is unknown.
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
from typing import NotRequired, TypedDict
|
||||
|
||||
|
||||
class UserProfile(TypedDict):
|
||||
user_id: str
|
||||
email: str
|
||||
created_at: datetime
|
||||
nickname: NotRequired[str]
|
||||
```
|
||||
|
||||
- For classes, declare all member variables explicitly with types at the top of the class body (before `__init__`), even when the class is not a dataclass or Pydantic model, so the class shape is obvious at a glance:
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Example:
|
||||
user_id: str
|
||||
created_at: datetime
|
||||
|
||||
def __init__(self, user_id: str, created_at: datetime) -> None:
|
||||
self.user_id = user_id
|
||||
self.created_at = created_at
|
||||
```
|
||||
|
||||
### General Rules
|
||||
|
||||
- Use Pydantic v2 conventions.
|
||||
- Use `uv` for Python package management in this repo (usually with `--project api`).
|
||||
- Prefer simple functions over small “utility classes” for lightweight helpers.
|
||||
- Avoid implementing dunder methods unless it’s clearly needed and matches existing patterns.
|
||||
- Never start long-running services as part of agent work (`uv run app.py`, `flask run`, etc.); running tests is allowed.
|
||||
- Keep files below ~800 lines; split when necessary.
|
||||
- Keep code readable and explicit—avoid clever hacks.
|
||||
|
||||
### Architecture & Boundaries
|
||||
|
||||
- Mirror the layered architecture: controller → service → core/domain.
|
||||
- Reuse existing helpers in `core/`, `services/`, and `libs/` before creating new abstractions.
|
||||
- Optimise for observability: deterministic control flow, clear logging, actionable errors.
|
||||
|
||||
### Owner-Bound Resource References
|
||||
|
||||
- Resolve and validate the outer owner before binding a nested resource ID.
|
||||
- For stable single-parent chains, use immutable nested `NamedTuple` refs.
|
||||
- Root refs carry tenant plus root ID; child refs carry the parent ref.
|
||||
- In production, construct refs through the domain ref service.
|
||||
- Python allowing direct construction does not grant authorization.
|
||||
- Scope every consuming query with complete owner predicates; refs are not security tokens.
|
||||
- Keep polymorphic owners flat until explicit nominal owner types exist.
|
||||
- Do not add generic ref bases or compatibility fields only for uniformity.
|
||||
- Reconstruct internal refs from validated database state after payload or async boundaries.
|
||||
|
||||
### Logging & Errors
|
||||
|
||||
- Never use `print`; use a module-level logger:
|
||||
- `logger = logging.getLogger(__name__)`
|
||||
- Include tenant/app/workflow identifiers in log context when relevant.
|
||||
- Raise domain-specific exceptions (`services/errors`, `core/errors`) and translate them into HTTP responses in controllers.
|
||||
- Log retryable events at `warning`, terminal failures at `error`.
|
||||
|
||||
### SQLAlchemy Patterns
|
||||
|
||||
- Models inherit from `models.base.TypeBase`; do not create ad-hoc metadata or engines.
|
||||
- Open sessions with context managers:
|
||||
|
||||
```python
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
stmt = select(Workflow).where(
|
||||
Workflow.id == workflow_id,
|
||||
Workflow.tenant_id == tenant_id,
|
||||
)
|
||||
workflow = session.execute(stmt).scalar_one_or_none()
|
||||
```
|
||||
|
||||
- Prefer SQLAlchemy expressions; avoid raw SQL unless necessary.
|
||||
- Always scope queries by `tenant_id` and protect write paths with safeguards (`FOR UPDATE`, row counts, etc.).
|
||||
- Introduce repository abstractions only for very large tables (e.g., workflow executions) or when alternative storage strategies are required.
|
||||
|
||||
### Storage & External I/O
|
||||
|
||||
- Access storage via `extensions.ext_storage.storage`.
|
||||
- Use `core.helper.ssrf_proxy` for outbound HTTP fetches.
|
||||
- Background tasks that touch storage must be idempotent, and should log relevant object identifiers.
|
||||
|
||||
### Pydantic Usage
|
||||
|
||||
- Define DTOs with Pydantic v2 models and forbid extras by default.
|
||||
- Use `@field_validator` / `@model_validator` for domain rules.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, ConfigDict, HttpUrl, field_validator
|
||||
|
||||
|
||||
class TriggerConfig(BaseModel):
|
||||
endpoint: HttpUrl
|
||||
secret: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
@field_validator("secret")
|
||||
def ensure_secret_prefix(cls, value: str) -> str:
|
||||
if not value.startswith("dify_"):
|
||||
raise ValueError("secret must start with dify_")
|
||||
return value
|
||||
```
|
||||
|
||||
### Generics & Protocols
|
||||
|
||||
- Use `typing.Protocol` to define behavioural contracts (e.g., cache interfaces).
|
||||
- Apply generics (`TypeVar`, `Generic`) for reusable utilities like caches or providers.
|
||||
- Validate dynamic inputs at runtime when generics cannot enforce safety alone.
|
||||
|
||||
### Tooling & Checks
|
||||
|
||||
Quick checks while iterating:
|
||||
|
||||
- Format: `make format`
|
||||
- Lint (includes auto-fix): `make lint`
|
||||
- Format and lint: `make lint`
|
||||
- Type check: `make type-check`
|
||||
- Unit tests: `make test`
|
||||
- Full backend tests, including Docker-backed suites: `make test-all`
|
||||
- Targeted tests: `make test TARGET_TESTS=./api/tests/<target_tests>`
|
||||
- Targeted tests: `make test TARGET_TESTS=./api/tests/<path>`
|
||||
|
||||
Before opening a PR / submitting:
|
||||
Run direct Python commands through `uv run --project api`. Docker-backed integration suites are normally CI-owned. Do not start long-running services as part of routine agent work.
|
||||
|
||||
- `make lint`
|
||||
- `make type-check`
|
||||
- `make test`
|
||||
## Architecture And Boundaries
|
||||
|
||||
### Controllers & Services
|
||||
|
||||
- Controllers: parse input via Pydantic, invoke services, return serialised responses; no business logic.
|
||||
- Services: coordinate repositories, providers, background tasks; keep side effects explicit.
|
||||
- Document non-obvious behaviour with concise docstrings and comments.
|
||||
- For `204 No Content` responses, return an empty body only; never return a dict, model, or other payload.
|
||||
- For Flask-RESTX controller request, query, and response schemas, follow `controllers/API_SCHEMA_GUIDE.md`.
|
||||
In short: use Pydantic models, document GET query params with `query_params_from_model(...)`, register response
|
||||
DTOs with `register_response_schema_models(...)`, serialize response DTOs with `dump_response(...)`,
|
||||
and avoid adding new legacy `ns.model(...)`, `@marshal_with(...)`, or GET `@ns.expect(...)` patterns.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Use `configs.dify_config` for configuration—never read environment variables directly.
|
||||
- Maintain tenant awareness end-to-end; `tenant_id` must flow through every layer touching shared resources.
|
||||
- Queue async work through `services/async_workflow_service`; implement tasks under `tasks/` with explicit queue selection.
|
||||
- Keep experimental scripts under `dev/`; do not ship them in production builds.
|
||||
- Keep transport parsing and serialization in controllers, orchestration in services, and domain policy in `core/` or its domain owner. Keep `libs/` business-agnostic and reuse existing owners before adding abstractions.
|
||||
- Before changing controller schemas, generated API contracts, or `SystemFeatureModel`, read `controllers/API_SCHEMA_GUIDE.md`. Treat `/system-features` as a minimal unauthenticated bootstrap allowlist, not a general configuration registry.
|
||||
- Scope tenant-owned reads and writes by the complete owner chain, and propagate `tenant_id` across every affected layer. Reconstruct trusted internal references from validated database state after payload or async boundaries.
|
||||
- Keep write transactions explicit and bounded. Do not perform external I/O inside an open transaction unless a documented consistency contract requires it.
|
||||
- Read configuration through `configs.dify_config`, access storage through `extensions.ext_storage.storage`, and route outbound HTTP through the existing SSRF-safe owner in `core.helper.ssrf_proxy`.
|
||||
- Use Pydantic v2 for request and response models. Reuse domain-specific exceptions and translate them at the controller boundary.
|
||||
- Use existing Celery task and queue owners for asynchronous work; do not route unrelated jobs through workflow-specific services.
|
||||
- Celery tasks that may be retried or redelivered must keep side effects idempotent and log affected resource identifiers.
|
||||
|
||||
+59
-30
@@ -1,10 +1,13 @@
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import NamedTuple
|
||||
|
||||
import socketio
|
||||
from flask import request
|
||||
from opentelemetry.trace import get_current_span
|
||||
from opentelemetry.trace.span import INVALID_SPAN_ID, INVALID_TRACE_ID
|
||||
from werkzeug.exceptions import Forbidden, HTTPException, ServiceUnavailable
|
||||
|
||||
from configs import dify_config
|
||||
from contexts.wrapper import RecyclableContextVar
|
||||
@@ -42,6 +45,53 @@ _CONSOLE_EXEMPT_PREFIXES = (
|
||||
"/console/api/activate/check",
|
||||
)
|
||||
|
||||
_WEBAPP_EXEMPT_PREFIXES = ("/api/system-features",)
|
||||
|
||||
_INVALID_LICENSE_STATUSES = (LicenseStatus.INACTIVE, LicenseStatus.EXPIRED, LicenseStatus.LOST)
|
||||
|
||||
|
||||
def _session_surface_error(license_status: LicenseStatus | None) -> HTTPException:
|
||||
if license_status is None:
|
||||
return UnauthorizedAndForceLogout("Unable to verify enterprise license. Please contact your administrator.")
|
||||
return UnauthorizedAndForceLogout(f"Enterprise license is {license_status}. Please contact your administrator.")
|
||||
|
||||
|
||||
def _bearer_surface_error(license_status: LicenseStatus | None) -> HTTPException:
|
||||
"""Token-authed: forcing a logout is meaningless and license state must not leak."""
|
||||
return Forbidden(description="license_required")
|
||||
|
||||
|
||||
def _retryable_surface_error(license_status: LicenseStatus | None) -> HTTPException:
|
||||
"""Webhook senders retry on 5xx but treat 4xx as permanent, disabling the subscription."""
|
||||
return ServiceUnavailable(description="license_required")
|
||||
|
||||
|
||||
class _LicenseGatedSurface(NamedTuple):
|
||||
prefix: str
|
||||
exempt_prefixes: tuple[str, ...]
|
||||
build_error: Callable[[LicenseStatus | None], HTTPException]
|
||||
|
||||
|
||||
# /files (plugin-daemon data plane), /inner/api (enterprise control plane) and /health
|
||||
# stay ungated: blocking them breaks workflow execution or license recovery itself.
|
||||
_LICENSE_GATED_SURFACES = (
|
||||
_LicenseGatedSurface("/console/api/", _CONSOLE_EXEMPT_PREFIXES, _session_surface_error),
|
||||
_LicenseGatedSurface("/api/", _WEBAPP_EXEMPT_PREFIXES, _session_surface_error),
|
||||
_LicenseGatedSurface("/v1", (), _bearer_surface_error),
|
||||
_LicenseGatedSurface("/mcp", (), _bearer_surface_error),
|
||||
_LicenseGatedSurface("/triggers", (), _retryable_surface_error),
|
||||
)
|
||||
|
||||
|
||||
def _match_license_gated_surface(path: str) -> _LicenseGatedSurface | None:
|
||||
for surface in _LICENSE_GATED_SURFACES:
|
||||
if not path.startswith(surface.prefix):
|
||||
continue
|
||||
if any(path.startswith(exempt) for exempt in surface.exempt_prefixes):
|
||||
return None
|
||||
return surface
|
||||
return None
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Application Factory Function
|
||||
@@ -62,38 +112,17 @@ def create_flask_app_with_configs() -> DifyApp:
|
||||
init_request_context()
|
||||
RecyclableContextVar.increment_thread_recycles()
|
||||
|
||||
# Enterprise license validation for API endpoints (both console and webapp)
|
||||
# When license expires, block all API access except bootstrap endpoints needed
|
||||
# for the frontend to load the license expiration page without infinite reloads.
|
||||
if dify_config.ENTERPRISE_ENABLED:
|
||||
is_console_api = request.path.startswith("/console/api/")
|
||||
is_webapp_api = request.path.startswith("/api/")
|
||||
surface = _match_license_gated_surface(request.path)
|
||||
if surface is not None:
|
||||
try:
|
||||
license_status = EnterpriseService.get_cached_license_status()
|
||||
except Exception:
|
||||
logger.exception("Failed to check enterprise license status")
|
||||
license_status = None
|
||||
|
||||
if is_console_api or is_webapp_api:
|
||||
if is_console_api:
|
||||
is_exempt = any(request.path.startswith(p) for p in _CONSOLE_EXEMPT_PREFIXES)
|
||||
else: # webapp API
|
||||
is_exempt = request.path.startswith("/api/system-features")
|
||||
|
||||
if not is_exempt:
|
||||
try:
|
||||
# Check license status (cached — see EnterpriseService for TTL details)
|
||||
license_status = EnterpriseService.get_cached_license_status()
|
||||
if license_status in (LicenseStatus.INACTIVE, LicenseStatus.EXPIRED, LicenseStatus.LOST):
|
||||
raise UnauthorizedAndForceLogout(
|
||||
f"Enterprise license is {license_status}. Please contact your administrator."
|
||||
)
|
||||
if license_status is None:
|
||||
raise UnauthorizedAndForceLogout(
|
||||
"Unable to verify enterprise license. Please contact your administrator."
|
||||
)
|
||||
except UnauthorizedAndForceLogout:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("Failed to check enterprise license status")
|
||||
raise UnauthorizedAndForceLogout(
|
||||
"Unable to verify enterprise license. Please contact your administrator."
|
||||
)
|
||||
if license_status is None or license_status in _INVALID_LICENSE_STATUSES:
|
||||
raise surface.build_error(license_status)
|
||||
|
||||
# add after request hook for injecting trace headers from OpenTelemetry span context
|
||||
# Only adds headers when OTEL is enabled and has valid context
|
||||
|
||||
@@ -11,6 +11,7 @@ from clients.agent_backend.fake_client import FakeAgentBackendRunClient, FakeAge
|
||||
def create_agent_backend_run_client(
|
||||
*,
|
||||
base_url: str | None = None,
|
||||
api_token: str | None = None,
|
||||
use_fake: bool = False,
|
||||
fake_scenario: str | FakeAgentBackendScenario = FakeAgentBackendScenario.SUCCESS,
|
||||
stream_read_timeout_seconds: float = 30,
|
||||
@@ -22,8 +23,11 @@ def create_agent_backend_run_client(
|
||||
return FakeAgentBackendRunClient(scenario=FakeAgentBackendScenario(fake_scenario))
|
||||
if base_url is None:
|
||||
raise ValueError("base_url is required when creating a real Agent backend client")
|
||||
headers: dict[str, str] = {}
|
||||
if api_token:
|
||||
headers["Authorization"] = f"Bearer {api_token}"
|
||||
return DifyAgentBackendRunClient(
|
||||
Client(base_url=base_url, stream_timeout=stream_read_timeout_seconds),
|
||||
Client(base_url=base_url, stream_timeout=stream_read_timeout_seconds, headers=headers),
|
||||
stream_max_reconnects=stream_max_reconnects,
|
||||
stream_timeout_seconds=stream_run_timeout_seconds,
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from sqlalchemy import delete, select, update
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from configs import dify_config
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from events.app_event import app_was_created
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
@@ -42,7 +43,7 @@ def reset_encrypt_key_pair():
|
||||
After the reset, all LLM credentials will become invalid, requiring re-entry.
|
||||
Only support SELF_HOSTED mode.
|
||||
"""
|
||||
if dify_config.EDITION != "SELF_HOSTED":
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||
click.echo(click.style("This command is only for SELF_HOSTED installations.", fg="red"))
|
||||
return
|
||||
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any, override
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource
|
||||
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from libs.file_utils import search_file_upwards
|
||||
|
||||
from .deploy import DeploymentConfig
|
||||
@@ -116,3 +117,11 @@ class DifyConfig(
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def DEPLOYMENT_EDITION(self) -> DeploymentEdition:
|
||||
if self.EDITION == "CLOUD":
|
||||
return DeploymentEdition.CLOUD
|
||||
if self.ENTERPRISE_ENABLED:
|
||||
return DeploymentEdition.ENTERPRISE
|
||||
return DeploymentEdition.COMMUNITY
|
||||
|
||||
@@ -12,6 +12,11 @@ class AgentBackendConfig(BaseSettings):
|
||||
default=None,
|
||||
)
|
||||
|
||||
AGENT_BACKEND_API_TOKEN: str | None = Field(
|
||||
description="Bearer token for authenticating with the Agent backend /runs API.",
|
||||
default=None,
|
||||
)
|
||||
|
||||
AGENT_BACKEND_USE_FAKE: bool = Field(
|
||||
description="Use the deterministic in-process fake Agent backend client.",
|
||||
default=False,
|
||||
|
||||
@@ -266,6 +266,12 @@ class PluginConfig(BaseSettings):
|
||||
default=60 * 60,
|
||||
)
|
||||
|
||||
PLUGIN_MODEL_PROVIDERS_CACHE_ENABLED: bool = Field(
|
||||
description="Whether tenant plugin model providers are cached in Redis. Disable when plugins are installed "
|
||||
"by a system other than this one, which cannot invalidate the cache when a tenant's plugins change.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
PLUGIN_MODEL_PROVIDERS_CACHE_TTL: PositiveInt = Field(
|
||||
description="TTL in seconds for caching tenant plugin model providers in Redis",
|
||||
default=60 * 60 * 24,
|
||||
@@ -816,6 +822,41 @@ class UpdateConfig(BaseSettings):
|
||||
)
|
||||
|
||||
|
||||
class CommunityTelemetryConfig(BaseSettings):
|
||||
"""
|
||||
Configuration for anonymous self-hosted community telemetry.
|
||||
"""
|
||||
|
||||
DISABLE_TELEMETRY: bool = Field(
|
||||
description="Disable anonymous community telemetry",
|
||||
default=False,
|
||||
)
|
||||
DO_NOT_TRACK: bool = Field(
|
||||
description="Respect the standard do-not-track opt-out signal for telemetry",
|
||||
default=False,
|
||||
)
|
||||
TELEMETRY_ENDPOINT: str = Field(
|
||||
description="Endpoint for anonymous community telemetry events",
|
||||
default="https://otel.dify.ai/v1/events",
|
||||
)
|
||||
TELEMETRY_FALLBACK_ENDPOINT: str = Field(
|
||||
description="Fallback endpoint for anonymous community telemetry events",
|
||||
default="https://otel.dify.cn/v1/events",
|
||||
)
|
||||
TELEMETRY_TIMEOUT_SECONDS: PositiveInt = Field(
|
||||
description="HTTP timeout in seconds for anonymous community telemetry requests",
|
||||
default=3,
|
||||
)
|
||||
TELEMETRY_HEARTBEAT_INTERVAL_MINUTES: PositiveInt = Field(
|
||||
description="Celery beat interval in minutes for checking whether heartbeat telemetry is due",
|
||||
default=30,
|
||||
)
|
||||
CI: bool = Field(
|
||||
description="Whether the process is running in CI; telemetry is skipped when true",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
class WorkflowVariableTruncationConfig(BaseSettings):
|
||||
WORKFLOW_VARIABLE_TRUNCATION_MAX_SIZE: PositiveInt = Field(
|
||||
# 1000 KiB
|
||||
@@ -1497,6 +1538,11 @@ class LoginConfig(BaseSettings):
|
||||
|
||||
|
||||
class AccountConfig(BaseSettings):
|
||||
ENABLE_CHANGE_EMAIL: bool = Field(
|
||||
description="whether users can change their email address",
|
||||
default=True,
|
||||
)
|
||||
|
||||
ACCOUNT_DELETION_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
|
||||
description="Duration in minutes for which a account deletion token remains valid",
|
||||
default=5,
|
||||
@@ -1594,6 +1640,7 @@ class FeatureConfig(
|
||||
TenantIsolatedTaskQueueConfig,
|
||||
ToolConfig,
|
||||
UpdateConfig,
|
||||
CommunityTelemetryConfig,
|
||||
WorkflowConfig,
|
||||
WorkflowNodeExecutionConfig,
|
||||
WorkspaceConfig,
|
||||
|
||||
@@ -12,6 +12,47 @@ parameters, response schemas, and Swagger documentation.
|
||||
- Do not add new Flask-RESTX `fields.*` dictionaries, `Namespace.model(...)` exports, or `@marshal_with(...)` for migrated or new endpoints.
|
||||
- Do not use `@ns.expect(...)` for GET query parameters. Flask-RESTX documents that as a request body.
|
||||
|
||||
## Public System Features Contract
|
||||
|
||||
The Console and Web `/system-features` endpoints share `SystemFeatureModel`. They are unauthenticated and may be
|
||||
requested during root SSR, so treat this response as a minimal public bootstrap allowlist. It is not a general
|
||||
configuration endpoint, a feature registry, or a mirror of environment and Enterprise settings. Existing fields are
|
||||
legacy inventory and do not establish precedent for new fields.
|
||||
|
||||
A new field is eligible only when all of the following are true:
|
||||
|
||||
1. Both Console and Web have named production consumers for the field.
|
||||
2. Both consumers need the value before authentication and tenant/workspace bootstrap to render initial state or
|
||||
choose an authentication flow.
|
||||
3. The value varies at runtime or by deployment and cannot be safely derived from an existing public contract.
|
||||
4. The value is non-sensitive, safe to disclose without authentication, and has stable public API semantics.
|
||||
5. Sending the value on every root bootstrap is demonstrably clearer and cheaper than a consumer-owned query.
|
||||
|
||||
Do not add:
|
||||
|
||||
- Backend-only policy or enforcement inputs, including security decisions, upload limits, or integration toggles.
|
||||
- Console-only or Web-only configuration.
|
||||
- Tenant, workspace, account, permission, billing-detail, or other post-authentication state.
|
||||
- Provider payloads, operational diagnostics, large nested objects, or values without active consumers.
|
||||
- Speculative fields added for possible future use.
|
||||
|
||||
Route excluded values to their actual owner:
|
||||
|
||||
- Keep backend enforcement behind a narrow service method or domain policy.
|
||||
- Serve post-authentication state from an authenticated domain endpoint.
|
||||
- Serve surface-specific bootstrap state from a Console- or Web-specific endpoint and account for its SSR, caching,
|
||||
and failure cost explicitly.
|
||||
- Load large, slow, or page-specific data lazily through a consumer-owned query.
|
||||
|
||||
Every pull request that adds a System Features field must:
|
||||
|
||||
- Name both production consumer paths and explain why they require the value before authentication.
|
||||
- Document the root SSR request, payload, caching, and failure-mode impact.
|
||||
- Update the Pydantic owner, regenerate OpenAPI Markdown and TypeScript/Zod contracts, and update shared fixtures.
|
||||
- Add Console and Web schema regression coverage. Do not hand-edit generated contracts or add compatibility defaults.
|
||||
|
||||
Reviewers should reject a field when its owner, pre-authentication need, or consumers are unclear.
|
||||
|
||||
## Naming
|
||||
|
||||
- Request body models: use a `Payload` suffix.
|
||||
@@ -121,6 +162,9 @@ That documents a GET request body and is not the expected contract.
|
||||
|
||||
## Responses
|
||||
|
||||
`204 No Content` responses must not serialize a response body. Return the status using the established controller pattern;
|
||||
do not return a dictionary, response model, or other payload.
|
||||
|
||||
Response models should inherit from `ResponseModel`:
|
||||
|
||||
```python
|
||||
|
||||
@@ -144,6 +144,7 @@ from .workspace import (
|
||||
models,
|
||||
plugin,
|
||||
rbac,
|
||||
skills,
|
||||
snippets,
|
||||
tool_providers,
|
||||
trigger_providers,
|
||||
@@ -225,6 +226,7 @@ __all__ = [
|
||||
"saved_message",
|
||||
"setup",
|
||||
"site",
|
||||
"skills",
|
||||
"snippet_workflow",
|
||||
"snippet_workflow_draft_variable",
|
||||
"snippets",
|
||||
|
||||
@@ -257,7 +257,6 @@ class AgentAppDetailWithSite(GenericAppDetailWithSite):
|
||||
debug_conversation_has_messages: bool = False
|
||||
debug_conversation_message_count: int = 0
|
||||
role: str | None = None
|
||||
active_config_is_published: bool = False
|
||||
|
||||
|
||||
class AgentDebugConversationRefreshResponse(BaseModel):
|
||||
@@ -410,10 +409,6 @@ def _serialize_agent_app_detail(
|
||||
payload["debug_conversation_has_messages"] = message_count > 0
|
||||
payload["debug_conversation_message_count"] = message_count
|
||||
payload["role"] = agent.role or ""
|
||||
payload["active_config_is_published"] = roster_service.active_config_is_published(
|
||||
tenant_id=app_model.tenant_id,
|
||||
agent=agent,
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ from services.app_service import (
|
||||
AppResponseView,
|
||||
AppService,
|
||||
CreateAppParams,
|
||||
RecentAppMode,
|
||||
StarredAppListParams,
|
||||
)
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
@@ -139,6 +140,10 @@ class AppListBaseQuery(BaseModel):
|
||||
raise ValueError("Invalid UUID format in creator_ids.") from exc
|
||||
|
||||
|
||||
class RecentAppListQuery(BaseModel):
|
||||
limit: int = Field(default=8, ge=1, le=8, description="Number of recently modified apps to return (1-8)")
|
||||
|
||||
|
||||
class AppListQuery(AppListBaseQuery):
|
||||
pass
|
||||
|
||||
@@ -411,6 +416,33 @@ class AppPartial(AppResponseModel):
|
||||
return to_timestamp(value)
|
||||
|
||||
|
||||
class RecentAppResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
icon_type: IconType | None = None
|
||||
icon: str | None = None
|
||||
icon_background: str | None = None
|
||||
mode: RecentAppMode
|
||||
author_name: str | None = None
|
||||
updated_at: int
|
||||
permission_keys: list[str] = Field(default_factory=list)
|
||||
maintainer: str | None = None
|
||||
|
||||
@computed_field(return_type=str | None) # type: ignore[prop-decorator]
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return build_icon_url(self.icon_type, self.icon)
|
||||
|
||||
@field_validator("updated_at", mode="before")
|
||||
@classmethod
|
||||
def _normalize_timestamp(cls, value: datetime | int) -> int:
|
||||
return to_timestamp(value)
|
||||
|
||||
|
||||
class RecentAppListResponse(ResponseModel):
|
||||
data: list[RecentAppResponse]
|
||||
|
||||
|
||||
class AppDetail(AppResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
@@ -575,6 +607,8 @@ register_schema_models(
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
AppPartial,
|
||||
RecentAppResponse,
|
||||
RecentAppListResponse,
|
||||
AppDetailWithSite,
|
||||
AppPagination,
|
||||
)
|
||||
@@ -699,6 +733,49 @@ class AppListApi(Resource):
|
||||
return app_detail.model_dump(mode="json"), 201
|
||||
|
||||
|
||||
@console_ns.route("/apps/recent")
|
||||
class RecentAppListApi(Resource):
|
||||
@console_ns.doc("list_recent_apps")
|
||||
@console_ns.doc(description="Get recently modified apps for the home Continue Work section")
|
||||
@console_ns.doc(params=query_params_from_model(RecentAppListQuery))
|
||||
@console_ns.response(200, "Success", console_ns.models[RecentAppListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
@with_session(write=False)
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, current_user_id: str, session: Session):
|
||||
"""Return the lightweight app cards needed by the Explore home page."""
|
||||
args = query_params_from_request(RecentAppListQuery)
|
||||
params = AppListParams(limit=args.limit)
|
||||
|
||||
permissions = enterprise_rbac_service.RBACService.MyPermissions.get(
|
||||
current_tenant_id,
|
||||
current_user_id,
|
||||
session=session,
|
||||
)
|
||||
if dify_config.RBAC_ENABLED:
|
||||
access_filter = resolve_app_access_filter(
|
||||
current_tenant_id,
|
||||
current_user_id,
|
||||
session=session,
|
||||
permissions=permissions,
|
||||
)
|
||||
access_filter.apply_to_params(params)
|
||||
|
||||
recent_apps = AppService().get_recent_apps(current_user_id, current_tenant_id, params, session)
|
||||
permission_keys_map = permissions.app.permission_keys_by_resource_ids([app.id for app in recent_apps])
|
||||
response_items = [
|
||||
RecentAppResponse.model_validate(app, from_attributes=True).model_copy(
|
||||
update={"permission_keys": permission_keys_map.get(app.id, [])}
|
||||
)
|
||||
for app in recent_apps
|
||||
]
|
||||
return dump_response(RecentAppListResponse, {"data": response_items}), 200
|
||||
|
||||
|
||||
@console_ns.route("/apps/starred")
|
||||
class StarredAppListApi(Resource):
|
||||
@console_ns.doc("list_starred_apps")
|
||||
|
||||
@@ -198,6 +198,6 @@ class ForgotPasswordResetApi(Resource):
|
||||
# Create workspace if needed
|
||||
if (
|
||||
not TenantService.get_join_tenants(account, session=db.session())
|
||||
and FeatureService.get_system_features().is_allow_create_workspace
|
||||
and FeatureService.is_workspace_creation_allowed()
|
||||
):
|
||||
TenantService.create_owner_tenant(account, session=db.session())
|
||||
|
||||
@@ -162,9 +162,10 @@ class LoginApi(Resource):
|
||||
# SELF_HOSTED only have one workspace
|
||||
tenants = TenantService.get_join_tenants(account, session=db.session())
|
||||
if len(tenants) == 0:
|
||||
system_features = FeatureService.get_system_features()
|
||||
|
||||
if system_features.is_allow_create_workspace and not system_features.license.workspaces.is_available():
|
||||
if (
|
||||
FeatureService.is_workspace_creation_allowed()
|
||||
and not FeatureService.get_license().workspaces.is_available()
|
||||
):
|
||||
raise WorkspacesLimitExceeded()
|
||||
else:
|
||||
return SimpleResultOptionalDataResponse(
|
||||
@@ -310,10 +311,10 @@ class EmailCodeLoginApi(Resource):
|
||||
if account:
|
||||
tenants = TenantService.get_join_tenants(account, session=db.session())
|
||||
if not tenants:
|
||||
workspaces = FeatureService.get_system_features().license.workspaces
|
||||
workspaces = FeatureService.get_license().workspaces
|
||||
if not workspaces.is_available():
|
||||
raise WorkspacesLimitExceeded()
|
||||
if not FeatureService.get_system_features().is_allow_create_workspace:
|
||||
if not FeatureService.is_workspace_creation_allowed():
|
||||
raise NotAllowedCreateWorkspace()
|
||||
else:
|
||||
TenantService.create_owner_tenant(account, session=db.session())
|
||||
|
||||
@@ -292,7 +292,7 @@ def _generate_account(
|
||||
if account:
|
||||
tenants = TenantService.get_join_tenants(account, session=db.session())
|
||||
if not tenants:
|
||||
if not FeatureService.get_system_features().is_allow_create_workspace:
|
||||
if not FeatureService.is_workspace_creation_allowed():
|
||||
raise WorkSpaceNotAllowedCreateError()
|
||||
else:
|
||||
TenantService.create_owner_tenant(account, session=db.session())
|
||||
|
||||
@@ -11,7 +11,7 @@ from controllers.console import console_ns
|
||||
from controllers.console.wraps import account_initialization_required, with_current_user
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import build_icon_url
|
||||
from libs.helper import build_icon_url, dump_response
|
||||
from libs.login import login_required
|
||||
from models import Account
|
||||
from services.recommended_app_service import RecommendedAppService
|
||||
@@ -58,7 +58,7 @@ class RecommendedAppResponse(ResponseModel):
|
||||
categories: list[str] = Field(default_factory=list)
|
||||
position: int | None = None
|
||||
is_listed: bool | None = None
|
||||
can_trial: bool | None = None
|
||||
can_trial: bool
|
||||
|
||||
|
||||
class RecommendedAppListResponse(ResponseModel):
|
||||
@@ -77,7 +77,7 @@ class RecommendedAppDetailResponse(ResponseModel):
|
||||
icon_background: str | None = None
|
||||
mode: str
|
||||
export_data: str
|
||||
can_trial: bool | None = None
|
||||
can_trial: bool
|
||||
|
||||
|
||||
class RecommendedAppDetailNullableResponse(RootModel[RecommendedAppDetailResponse | None]):
|
||||
@@ -119,10 +119,10 @@ class RecommendedAppListApi(Resource):
|
||||
args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True))
|
||||
language_prefix = _resolve_language(args.language, current_user)
|
||||
|
||||
return RecommendedAppListResponse.model_validate(
|
||||
return dump_response(
|
||||
RecommendedAppListResponse,
|
||||
RecommendedAppService.get_recommended_apps_and_categories(language_prefix, session=db.session()),
|
||||
from_attributes=True,
|
||||
).model_dump(mode="json")
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/explore/apps/learn-dify")
|
||||
@@ -136,10 +136,10 @@ class LearnDifyAppListApi(Resource):
|
||||
args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True))
|
||||
language_prefix = _resolve_language(args.language, current_user)
|
||||
|
||||
return LearnDifyAppListResponse.model_validate(
|
||||
return dump_response(
|
||||
LearnDifyAppListResponse,
|
||||
RecommendedAppService.get_learn_dify_apps(language_prefix, session=db.session()),
|
||||
from_attributes=True,
|
||||
).model_dump(mode="json")
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/explore/apps/<uuid:app_id>")
|
||||
@@ -148,4 +148,5 @@ class RecommendedAppApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self, app_id: UUID):
|
||||
return RecommendedAppService.get_recommend_app_detail(str(app_id), session=db.session())
|
||||
result = RecommendedAppService.get_recommend_app_detail(str(app_id), session=db.session())
|
||||
return RecommendedAppDetailNullableResponse.model_validate(result).model_dump(mode="json")
|
||||
|
||||
@@ -46,7 +46,7 @@ from controllers.console.explore.error import (
|
||||
NotCompletionAppError,
|
||||
NotWorkflowAppError,
|
||||
)
|
||||
from controllers.console.explore.wraps import TrialAppResource, trial_feature_enable
|
||||
from controllers.console.explore.wraps import TrialAppResource
|
||||
from controllers.console.files import FILE_UPLOAD_PARAMS, upload_file_from_request
|
||||
from controllers.console.remote_files import RemoteFileUploadPayload, upload_remote_file_from_request
|
||||
from controllers.console.wraps import cloud_edition_billing_resource_check, with_current_user
|
||||
@@ -432,7 +432,6 @@ simple_account_model = console_ns.models[TrialSimpleAccount.__name__]
|
||||
|
||||
|
||||
class TrialAppFileUploadApi(TrialAppResource):
|
||||
@trial_feature_enable
|
||||
@cloud_edition_billing_resource_check("documents")
|
||||
@console_ns.doc(consumes=["multipart/form-data"], params=FILE_UPLOAD_PARAMS)
|
||||
@console_ns.response(201, "File uploaded successfully", console_ns.models[FileResponse.__name__])
|
||||
@@ -447,7 +446,6 @@ class TrialAppFileUploadApi(TrialAppResource):
|
||||
|
||||
|
||||
class TrialAppRemoteFileUploadApi(TrialAppResource):
|
||||
@trial_feature_enable
|
||||
@cloud_edition_billing_resource_check("documents")
|
||||
@console_ns.expect(console_ns.models[RemoteFileUploadPayload.__name__])
|
||||
@console_ns.response(201, "File uploaded successfully", console_ns.models[FileWithSignedUrl.__name__])
|
||||
@@ -462,7 +460,6 @@ class TrialAppRemoteFileUploadApi(TrialAppResource):
|
||||
|
||||
|
||||
class TrialAppWorkflowRunApi(TrialAppResource):
|
||||
@trial_feature_enable
|
||||
@console_ns.expect(console_ns.models[WorkflowRunRequest.__name__])
|
||||
@console_ns.response(200, "Success")
|
||||
@with_current_user
|
||||
@@ -513,7 +510,6 @@ class TrialAppWorkflowRunApi(TrialAppResource):
|
||||
|
||||
class TrialAppWorkflowTaskStopApi(TrialAppResource):
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@trial_feature_enable
|
||||
def post(self, trial_app, task_id: str):
|
||||
"""
|
||||
Stop workflow task
|
||||
@@ -538,7 +534,6 @@ class TrialAppWorkflowTaskStopApi(TrialAppResource):
|
||||
class TrialChatApi(TrialAppResource):
|
||||
@console_ns.expect(console_ns.models[ChatRequest.__name__])
|
||||
@console_ns.response(200, "Success")
|
||||
@trial_feature_enable
|
||||
@with_current_user
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account, trial_app):
|
||||
@@ -640,7 +635,6 @@ class TrialMessageSuggestedQuestionApi(TrialAppResource):
|
||||
|
||||
class TrialChatAudioApi(TrialAppResource):
|
||||
@console_ns.response(200, "Success", console_ns.models[AudioTranscriptResponse.__name__])
|
||||
@trial_feature_enable
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, trial_app):
|
||||
app_model = trial_app
|
||||
@@ -691,7 +685,6 @@ class TrialChatAudioApi(TrialAppResource):
|
||||
class TrialChatTextApi(TrialAppResource):
|
||||
@console_ns.expect(console_ns.models[TextToSpeechRequest.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[AudioBinaryResponse.__name__])
|
||||
@trial_feature_enable
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, trial_app):
|
||||
app_model = trial_app
|
||||
@@ -752,7 +745,6 @@ class TrialChatTextApi(TrialAppResource):
|
||||
class TrialCompletionApi(TrialAppResource):
|
||||
@console_ns.expect(console_ns.models[CompletionRequest.__name__])
|
||||
@console_ns.response(200, "Success")
|
||||
@trial_feature_enable
|
||||
@with_current_user
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account, trial_app):
|
||||
|
||||
@@ -14,6 +14,7 @@ from libs.login import current_account_with_tenant, login_required
|
||||
from models import AccountTrialAppRecord, App, InstalledApp, TrialApp
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.feature_service import FeatureService
|
||||
from services.recommended_app_service import RecommendedAppService
|
||||
|
||||
|
||||
def installed_app_required[**P, R](view: Callable[Concatenate[InstalledApp, P], R] | None = None):
|
||||
@@ -106,8 +107,7 @@ def trial_app_required[**P, R](view: Callable[Concatenate[App, P], R] | None = N
|
||||
def trial_feature_enable[**P, R](view: Callable[P, R]):
|
||||
@wraps(view)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||
features = FeatureService.get_system_features()
|
||||
if not features.enable_trial_app:
|
||||
if not RecommendedAppService.is_trial_app_enabled():
|
||||
abort(403, "Trial app feature is not enabled.")
|
||||
return view(*args, **kwargs)
|
||||
|
||||
@@ -141,6 +141,7 @@ class TrialAppResource(Resource):
|
||||
|
||||
method_decorators = [
|
||||
trial_app_required,
|
||||
trial_feature_enable,
|
||||
account_initialization_required,
|
||||
login_required,
|
||||
]
|
||||
|
||||
@@ -3,10 +3,11 @@ from flask_restx import Resource
|
||||
from controllers.common.schema import register_response_schema_models
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import dump_response
|
||||
from libs.login import current_account_with_tenant_optional, login_required
|
||||
from libs.login import login_required
|
||||
from services.feature_service import (
|
||||
FeatureModel,
|
||||
FeatureService,
|
||||
LicenseModel,
|
||||
LimitationModel,
|
||||
SystemFeatureModel,
|
||||
)
|
||||
@@ -32,6 +33,7 @@ register_response_schema_models(
|
||||
console_ns,
|
||||
AppDslVersionResponse,
|
||||
FeatureModel,
|
||||
LicenseModel,
|
||||
LimitationModel,
|
||||
SystemFeatureModel,
|
||||
TrialModelsResponse,
|
||||
@@ -121,22 +123,40 @@ class AppDslVersionApi(Resource):
|
||||
@console_ns.route("/system-features")
|
||||
class SystemFeatureApi(Resource):
|
||||
@console_ns.doc("get_system_features")
|
||||
@console_ns.doc(description="Get system-wide feature configuration")
|
||||
@console_ns.doc(
|
||||
description="Get the non-sensitive bootstrap snapshot exposed before Console or Web authentication. "
|
||||
"This is not a general feature registry."
|
||||
)
|
||||
@console_ns.response(
|
||||
200,
|
||||
"Success",
|
||||
console_ns.models[SystemFeatureModel.__name__],
|
||||
)
|
||||
def get(self):
|
||||
"""Get system-wide feature configuration
|
||||
"""Get the non-sensitive bootstrap snapshot exposed before authentication.
|
||||
|
||||
NOTE: This endpoint is unauthenticated by design, as it provides system features
|
||||
data required for dashboard initialization.
|
||||
|
||||
Authentication would create circular dependency (can't login without dashboard loading).
|
||||
|
||||
Only non-sensitive configuration data should be returned by this endpoint.
|
||||
Authentication configuration must be available before the authentication flow can be selected.
|
||||
Authenticated license detail is served separately by SystemFeatureLicenseApi.
|
||||
"""
|
||||
current_user, _ = current_account_with_tenant_optional()
|
||||
is_authenticated = current_user is not None
|
||||
return FeatureService.get_system_features(is_authenticated=is_authenticated).model_dump()
|
||||
return dump_response(SystemFeatureModel, FeatureService.get_system_features())
|
||||
|
||||
|
||||
@console_ns.route("/system-features/license")
|
||||
class SystemFeatureLicenseApi(Resource):
|
||||
@console_ns.doc("get_system_license")
|
||||
@console_ns.doc(description="Get license status and usage detail")
|
||||
@console_ns.response(
|
||||
200,
|
||||
"Success",
|
||||
console_ns.models[LicenseModel.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
"""Get full license detail (status, expiry, workspace/seat usage).
|
||||
|
||||
Authenticated counterpart to the license *status* exposed on the public
|
||||
system-features endpoint.
|
||||
"""
|
||||
return FeatureService.get_license().model_dump()
|
||||
|
||||
@@ -8,6 +8,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.fastopenapi import console_router
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from extensions.ext_database import db
|
||||
from models.model import DifySetup
|
||||
from services.account_service import TenantService
|
||||
@@ -63,7 +64,7 @@ def validate_init_password(payload: InitValidatePayload) -> InitValidateResponse
|
||||
|
||||
|
||||
def get_init_validate_status() -> bool:
|
||||
if dify_config.EDITION == "SELF_HOSTED":
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||
if os.environ.get("INIT_PASSWORD"):
|
||||
if session.get("is_init_validated"):
|
||||
return True
|
||||
|
||||
@@ -6,6 +6,7 @@ from sqlalchemy import select
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.fastopenapi import console_router
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from libs.helper import EmailStr, extract_remote_ip
|
||||
from libs.password import valid_password
|
||||
from models.model import DifySetup, db
|
||||
@@ -52,7 +53,7 @@ def get_setup_status_api() -> SetupStatusResponse:
|
||||
|
||||
Only bootstrap-safe status information should be returned by this endpoint.
|
||||
"""
|
||||
if dify_config.EDITION == "SELF_HOSTED":
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||
setup_status = get_setup_status()
|
||||
if setup_status and not isinstance(setup_status, bool):
|
||||
return SetupStatusResponse(step="finished", setup_at=setup_status.setup_at.isoformat())
|
||||
@@ -102,7 +103,7 @@ def setup_system(payload: SetupRequestPayload) -> SetupResponse:
|
||||
|
||||
|
||||
def get_setup_status() -> DifySetup | bool | None:
|
||||
if dify_config.EDITION == "SELF_HOSTED":
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||
return db.session.scalar(select(DifySetup).limit(1))
|
||||
|
||||
return True
|
||||
|
||||
@@ -59,7 +59,7 @@ class TagBindingRemovePayload(BaseModel):
|
||||
|
||||
|
||||
class TagListQueryParam(BaseModel):
|
||||
type: Literal["knowledge", "app", "snippet", ""] = Field("", description="Tag type filter")
|
||||
type: TagType | Literal[""] = Field("", description="Tag type filter")
|
||||
keyword: str | None = Field(None, description="Search keyword")
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ from controllers.console.wraps import (
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from fields.member_fields import AccountResponse
|
||||
@@ -262,7 +263,7 @@ class AccountInitApi(Resource):
|
||||
payload = console_ns.payload or {}
|
||||
args = AccountInitPayload.model_validate(payload)
|
||||
|
||||
if dify_config.EDITION == "CLOUD":
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||
if not args.invitation_code:
|
||||
raise ValueError("invitation_code is required")
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ def _check_member_invite_limits(tenant_id: str, new_member_count: int, new_accou
|
||||
if workspace_members.enabled is True and not workspace_members.is_available(new_member_count):
|
||||
raise WorkspaceMembersLimitExceeded()
|
||||
if new_account_count > 0:
|
||||
seats = FeatureService.get_system_features(is_authenticated=True).license.seats
|
||||
seats = FeatureService.get_license().seats
|
||||
if not seats.is_available(new_account_count):
|
||||
raise SeatsLimitExceeded()
|
||||
return
|
||||
|
||||
@@ -442,12 +442,10 @@ register_enum_models(
|
||||
)
|
||||
|
||||
|
||||
def _default_auto_upgrade_settings(
|
||||
tenant_id: str,
|
||||
category: TenantPluginAutoUpgradeCategory,
|
||||
) -> AutoUpgradeSettingsResponse:
|
||||
def _missing_auto_upgrade_settings(tenant_id: str) -> AutoUpgradeSettingsResponse:
|
||||
"""Represent a missing persisted strategy as effectively disabled."""
|
||||
return {
|
||||
"strategy_setting": PluginAutoUpgradeService.default_strategy_setting_for_category(category),
|
||||
"strategy_setting": TenantPluginAutoUpgradeStrategySetting.DISABLED,
|
||||
"upgrade_time_of_day": PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id),
|
||||
"upgrade_mode": TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
"exclude_plugins": [],
|
||||
@@ -1135,9 +1133,7 @@ class PluginFetchAutoUpgradeApi(Resource):
|
||||
args = ParserAutoUpgradeFetch.model_validate(request.args.to_dict(flat=True))
|
||||
auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id, args.category, session=db.session())
|
||||
auto_upgrade_dict = (
|
||||
_auto_upgrade_settings_to_dict(auto_upgrade)
|
||||
if auto_upgrade
|
||||
else _default_auto_upgrade_settings(tenant_id, args.category)
|
||||
_auto_upgrade_settings_to_dict(auto_upgrade) if auto_upgrade else _missing_auto_upgrade_settings(tenant_id)
|
||||
)
|
||||
|
||||
return jsonable_encoder(
|
||||
|
||||
@@ -0,0 +1,834 @@
|
||||
"""Console API for workspace-level Skill Management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
|
||||
from flask import request, send_file
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
|
||||
from controllers.common.fields import BinaryFileResponse
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
from fields.base import ResponseModel
|
||||
from libs import helper
|
||||
from libs.helper import dump_response
|
||||
from libs.login import login_required
|
||||
from models.account import Account
|
||||
from services.skill_management_service import (
|
||||
SkillAssistMessagePayload,
|
||||
SkillCreatePayload,
|
||||
SkillDraftFileOperationPayload,
|
||||
SkillDraftTreePayload,
|
||||
SkillImportPayload,
|
||||
SkillManagementService,
|
||||
SkillManagementServiceError,
|
||||
SkillMetadataPayload,
|
||||
SkillPublishPayload,
|
||||
SkillRestorePayload,
|
||||
SkillVersionUpdatePayload,
|
||||
)
|
||||
|
||||
_FILE_UPLOAD_PARAMS = {
|
||||
"file": {
|
||||
"description": "Skill draft file payload",
|
||||
"in": "formData",
|
||||
"type": "file",
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class WorkspaceSkillsQuery(BaseModel):
|
||||
keyword: str | None = Field(default=None, description="Search keyword matching skill name or description.")
|
||||
page: int = Field(default=1, ge=1, le=99999, description="Page number.")
|
||||
limit: int = Field(default=20, ge=1, le=100, description="Number of items per page.")
|
||||
tag: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Skill tag filters. Repeat the parameter for multiple tags.",
|
||||
)
|
||||
|
||||
|
||||
class SkillDeletePayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
confirmation_name: str | None = Field(
|
||||
default=None,
|
||||
description="Required when deleting a referenced Skill. Must match the Skill name.",
|
||||
)
|
||||
|
||||
|
||||
class AgentSkillBindingsPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
skill_ids: list[str] = Field(default_factory=list, description="Ordered Skill IDs bound to the Agent.")
|
||||
|
||||
|
||||
class SkillFileQuery(BaseModel):
|
||||
path: str = Field(description="Skill file path relative to the Skill root.")
|
||||
version_id: str | None = Field(default=None, description="Optional published version ID. Omit for current draft.")
|
||||
|
||||
|
||||
class SkillResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
display_name: str
|
||||
icon: str
|
||||
description: str
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
name_manually_edited: bool = False
|
||||
visibility: str
|
||||
latest_published_version_id: str | None = None
|
||||
reference_count: int = 0
|
||||
created_by: str | None = None
|
||||
created_by_name: str | None = None
|
||||
updated_by: str | None = None
|
||||
updated_by_name: str | None = None
|
||||
created_at: int
|
||||
updated_at: int
|
||||
|
||||
|
||||
class SkillFileResponse(ResponseModel):
|
||||
id: str | None = None
|
||||
path: str
|
||||
kind: str
|
||||
storage: str | None = None
|
||||
mime_type: str | None = None
|
||||
content: str | None = None
|
||||
tool_file_id: str | None = None
|
||||
size: int | None = None
|
||||
hash: str | None = None
|
||||
|
||||
|
||||
class SkillFilePreviewResponse(ResponseModel):
|
||||
path: str
|
||||
mime_type: str
|
||||
content: str
|
||||
size: int
|
||||
hash: str
|
||||
|
||||
|
||||
class SkillFileUploadResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
mime_type: str
|
||||
size: int
|
||||
hash: str
|
||||
|
||||
|
||||
class SkillDetailResponse(SkillResponse):
|
||||
files: list[SkillFileResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillListResponse(ResponseModel):
|
||||
data: list[SkillResponse] = Field(default_factory=list)
|
||||
has_more: bool = False
|
||||
limit: int = 20
|
||||
page: int = 1
|
||||
total: int = 0
|
||||
|
||||
|
||||
class SkillTagResponse(ResponseModel):
|
||||
tag: str
|
||||
count: int
|
||||
|
||||
|
||||
class SkillTagListResponse(ResponseModel):
|
||||
data: list[SkillTagResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillVersionResponse(ResponseModel):
|
||||
id: str
|
||||
skill_id: str
|
||||
version_number: int
|
||||
version_name: str
|
||||
publish_note: str
|
||||
hash_code: str
|
||||
archive_size: int
|
||||
published_by: str | None = None
|
||||
published_by_name: str | None = None
|
||||
is_latest: bool = False
|
||||
created_at: int
|
||||
|
||||
|
||||
class SkillVersionListResponse(ResponseModel):
|
||||
data: list[SkillVersionResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillVersionDetailResponse(SkillVersionResponse):
|
||||
files: list[SkillFileResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillVersionDeleteResponse(ResponseModel):
|
||||
id: str
|
||||
deleted: bool
|
||||
latest_published_version_id: str | None = None
|
||||
|
||||
|
||||
class SkillReferenceResponse(ResponseModel):
|
||||
type: str
|
||||
agent_id: str
|
||||
agent_icon: str | None = None
|
||||
agent_icon_background: str | None = None
|
||||
agent_icon_type: str | None = None
|
||||
app_id: str | None = None
|
||||
name: str
|
||||
display_name: str
|
||||
workflow_id: str | None = None
|
||||
workflow_name: str | None = None
|
||||
workflow_icon: str | None = None
|
||||
workflow_icon_background: str | None = None
|
||||
workflow_icon_type: str | None = None
|
||||
workflow_version: str | None = None
|
||||
node_id: str | None = None
|
||||
node_name: str | None = None
|
||||
|
||||
|
||||
class SkillReferenceListResponse(ResponseModel):
|
||||
data: list[SkillReferenceResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillDeleteResponse(ResponseModel):
|
||||
id: str
|
||||
deleted: bool
|
||||
|
||||
|
||||
class AgentSkillBindingItemResponse(ResponseModel):
|
||||
id: str
|
||||
priority: int
|
||||
name: str
|
||||
display_name: str
|
||||
icon: str
|
||||
description: str
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
status: str
|
||||
file_count: int
|
||||
latest_published_version_id: str | None = None
|
||||
latest_published_at: int | None = None
|
||||
updated_at: int
|
||||
|
||||
|
||||
class AgentSkillBindingsResponse(ResponseModel):
|
||||
agent_id: str
|
||||
skill_ids: list[str] = Field(default_factory=list)
|
||||
data: list[AgentSkillBindingItemResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
WorkspaceSkillsQuery,
|
||||
SkillCreatePayload,
|
||||
SkillAssistMessagePayload,
|
||||
SkillMetadataPayload,
|
||||
SkillDraftFileOperationPayload,
|
||||
SkillDraftTreePayload,
|
||||
SkillPublishPayload,
|
||||
SkillRestorePayload,
|
||||
SkillVersionUpdatePayload,
|
||||
SkillDeletePayload,
|
||||
SkillFileQuery,
|
||||
AgentSkillBindingsPayload,
|
||||
)
|
||||
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
SkillResponse,
|
||||
SkillFileResponse,
|
||||
SkillFilePreviewResponse,
|
||||
SkillFileUploadResponse,
|
||||
SkillDetailResponse,
|
||||
SkillListResponse,
|
||||
SkillTagResponse,
|
||||
SkillTagListResponse,
|
||||
SkillVersionResponse,
|
||||
SkillVersionListResponse,
|
||||
SkillVersionDetailResponse,
|
||||
SkillVersionDeleteResponse,
|
||||
SkillReferenceResponse,
|
||||
SkillReferenceListResponse,
|
||||
SkillDeleteResponse,
|
||||
AgentSkillBindingItemResponse,
|
||||
AgentSkillBindingsResponse,
|
||||
BinaryFileResponse,
|
||||
)
|
||||
|
||||
|
||||
def _error_response(exc: SkillManagementServiceError) -> tuple[dict[str, object], int]:
|
||||
body: dict[str, object] = {"code": exc.code, "message": exc.message}
|
||||
if exc.details:
|
||||
body["details"] = exc.details
|
||||
return body, exc.status_code
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills")
|
||||
class WorkspaceSkillsApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(WorkspaceSkillsQuery))
|
||||
@console_ns.response(200, "Workspace skills", console_ns.models[SkillListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str):
|
||||
query_input: dict[str, object] = {
|
||||
"keyword": request.args.get("keyword"),
|
||||
"tag": request.args.getlist("tag"),
|
||||
}
|
||||
if "limit" in request.args:
|
||||
query_input["limit"] = request.args.get("limit")
|
||||
if "page" in request.args:
|
||||
query_input["page"] = request.args.get("page")
|
||||
query = WorkspaceSkillsQuery.model_validate(query_input)
|
||||
result = SkillManagementService().list_skills(
|
||||
tenant_id=current_tenant_id,
|
||||
keyword=query.keyword,
|
||||
page=query.page,
|
||||
limit=query.limit,
|
||||
tags=[tag for tag in query.tag if tag],
|
||||
)
|
||||
return dump_response(SkillListResponse, result)
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillCreatePayload.__name__])
|
||||
@console_ns.response(201, "Skill created", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
try:
|
||||
payload = SkillCreatePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().create_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillDetailResponse, result), 201
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/files/upload")
|
||||
class WorkspaceSkillFileUploadApi(Resource):
|
||||
@console_ns.doc(consumes=["multipart/form-data"], params=_FILE_UPLOAD_PARAMS)
|
||||
@console_ns.response(201, "Skill draft file uploaded", console_ns.models[SkillFileUploadResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
if "file" not in request.files:
|
||||
return {"code": "no_file_uploaded", "message": "no file uploaded"}, 400
|
||||
|
||||
file = request.files["file"]
|
||||
if not file.filename:
|
||||
return {"code": "filename_missing", "message": "filename is required"}, 400
|
||||
|
||||
result = SkillManagementService().upload_file(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
filename=file.filename,
|
||||
content=file.stream.read(),
|
||||
mime_type=file.mimetype,
|
||||
)
|
||||
return dump_response(SkillFileUploadResponse, result), 201
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/tags")
|
||||
class WorkspaceSkillTagsApi(Resource):
|
||||
@console_ns.response(200, "Workspace Skill tags", console_ns.models[SkillTagListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str):
|
||||
result = SkillManagementService().list_tags(tenant_id=current_tenant_id)
|
||||
return dump_response(SkillTagListResponse, result)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/import")
|
||||
class WorkspaceSkillImportApi(Resource):
|
||||
@console_ns.doc(description="Import a Skill zip package from multipart form field `file`.")
|
||||
@console_ns.response(201, "Skill imported", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
upload = request.files.get("file")
|
||||
if upload is None:
|
||||
return {"code": "invalid_request", "message": "file is required"}, 400
|
||||
try:
|
||||
payload = SkillImportPayload(content=upload.read(), filename=upload.filename or "skill.zip")
|
||||
result = SkillManagementService().import_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillDetailResponse, result), 201
|
||||
except (ValidationError, ValueError) as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>")
|
||||
class WorkspaceSkillApi(Resource):
|
||||
@console_ns.response(200, "Skill detail", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().get_skill(tenant_id=current_tenant_id, skill_id=skill_id)
|
||||
return dump_response(SkillDetailResponse, result)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillMetadataPayload.__name__])
|
||||
@console_ns.response(200, "Skill updated", console_ns.models[SkillResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillMetadataPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().update_metadata(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillDeletePayload.__name__])
|
||||
@console_ns.response(200, "Skill deleted", console_ns.models[SkillDeleteResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
payload = SkillDeletePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().delete_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
confirmation_name=payload.confirmation_name,
|
||||
)
|
||||
return dump_response(SkillDeleteResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/duplicate")
|
||||
class WorkspaceSkillDuplicateApi(Resource):
|
||||
@console_ns.response(201, "Skill duplicated", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().duplicate_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
)
|
||||
return dump_response(SkillDetailResponse, result), 201
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/export")
|
||||
class WorkspaceSkillExportApi(Resource):
|
||||
@console_ns.response(200, "Published Skill zip archive")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().pull_published_archive(tenant_id=current_tenant_id, skill_id=skill_id)
|
||||
return send_file(
|
||||
io.BytesIO(result.payload),
|
||||
mimetype=result.mime_type,
|
||||
as_attachment=True,
|
||||
download_name=result.filename,
|
||||
)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/assist/messages")
|
||||
class WorkspaceSkillAssistMessageApi(Resource):
|
||||
"""Stream read-only Skill Authoring suggestions from the default workspace model."""
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillAssistMessagePayload.__name__])
|
||||
@console_ns.response(200, "Skill Authoring assistant event stream")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillAssistMessagePayload.model_validate(console_ns.payload or {})
|
||||
response = SkillManagementService().create_assistant_action_stream(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
user_id=current_user.id,
|
||||
message=payload.message,
|
||||
attachments=payload.attachments,
|
||||
model_payload=payload.model,
|
||||
target_path=payload.target_path,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
return helper.compact_generate_response(response)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files")
|
||||
class WorkspaceSkillFilesApi(Resource):
|
||||
@console_ns.expect(console_ns.models[SkillDraftFileOperationPayload.__name__])
|
||||
@console_ns.response(200, "Draft file operation applied", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillDraftFileOperationPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().apply_draft_file_operation(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillDetailResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillDraftTreePayload.__name__])
|
||||
@console_ns.response(200, "Draft files replaced", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def put(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillDraftTreePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().replace_draft_tree(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillDetailResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files/preview")
|
||||
class WorkspaceSkillFilePreviewApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(SkillFileQuery))
|
||||
@console_ns.response(200, "Skill file text preview", console_ns.models[SkillFilePreviewResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
query = SkillFileQuery.model_validate(
|
||||
{
|
||||
"path": request.args.get("path"),
|
||||
"version_id": request.args.get("version_id"),
|
||||
}
|
||||
)
|
||||
result = SkillManagementService().preview_file(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
path=query.path,
|
||||
version_id=query.version_id,
|
||||
)
|
||||
return dump_response(SkillFilePreviewResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files/content")
|
||||
class WorkspaceSkillFileContentApi(Resource):
|
||||
@console_ns.doc(params={**query_params_from_model(SkillFileQuery), "download": "Return as an attachment when 1."})
|
||||
@console_ns.response(200, "Skill file content", console_ns.models[BinaryFileResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
query = SkillFileQuery.model_validate(
|
||||
{
|
||||
"path": request.args.get("path"),
|
||||
"version_id": request.args.get("version_id"),
|
||||
}
|
||||
)
|
||||
result = SkillManagementService().pull_file(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
path=query.path,
|
||||
version_id=query.version_id,
|
||||
)
|
||||
return send_file(
|
||||
io.BytesIO(result.payload),
|
||||
mimetype=result.mime_type,
|
||||
as_attachment=request.args.get("download") == "1",
|
||||
download_name=result.filename,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/publish")
|
||||
class WorkspaceSkillPublishApi(Resource):
|
||||
@console_ns.expect(console_ns.models[SkillPublishPayload.__name__])
|
||||
@console_ns.response(200, "Skill published", console_ns.models[SkillVersionResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillPublishPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().publish_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillVersionResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/restore")
|
||||
class WorkspaceSkillRestoreApi(Resource):
|
||||
@console_ns.expect(console_ns.models[SkillRestorePayload.__name__])
|
||||
@console_ns.response(200, "Skill version restored", console_ns.models[SkillVersionResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillRestorePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().restore_version(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillVersionResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/references")
|
||||
class WorkspaceSkillReferencesApi(Resource):
|
||||
@console_ns.response(200, "Skill references", console_ns.models[SkillReferenceListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().list_skill_references(tenant_id=current_tenant_id, skill_id=skill_id)
|
||||
return dump_response(SkillReferenceListResponse, result)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/versions")
|
||||
class WorkspaceSkillVersionsApi(Resource):
|
||||
@console_ns.response(200, "Skill versions", console_ns.models[SkillVersionListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().list_versions(tenant_id=current_tenant_id, skill_id=skill_id)
|
||||
return dump_response(SkillVersionListResponse, result)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/versions/<string:version_id>")
|
||||
class WorkspaceSkillVersionApi(Resource):
|
||||
@console_ns.response(200, "Skill version detail", console_ns.models[SkillVersionDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str, version_id: str):
|
||||
try:
|
||||
result = SkillManagementService().get_version(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
version_id=version_id,
|
||||
)
|
||||
return dump_response(SkillVersionDetailResponse, result)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillVersionUpdatePayload.__name__])
|
||||
@console_ns.response(200, "Skill version updated", console_ns.models[SkillVersionResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, skill_id: str, version_id: str):
|
||||
try:
|
||||
payload = SkillVersionUpdatePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().update_version(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
version_id=version_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillVersionResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
@console_ns.response(200, "Skill version deleted", console_ns.models[SkillVersionDeleteResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, current_user: Account, skill_id: str, version_id: str):
|
||||
try:
|
||||
result = SkillManagementService().delete_version(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
version_id=version_id,
|
||||
)
|
||||
return dump_response(SkillVersionDeleteResponse, result)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/agents/<string:agent_id>/skills")
|
||||
class WorkspaceAgentSkillBindingsApi(Resource):
|
||||
@console_ns.response(200, "Agent Skill bindings", console_ns.models[AgentSkillBindingsResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, agent_id: str):
|
||||
result = SkillManagementService().list_agent_bindings(tenant_id=current_tenant_id, agent_id=agent_id)
|
||||
return dump_response(AgentSkillBindingsResponse, result)
|
||||
|
||||
@console_ns.expect(console_ns.models[AgentSkillBindingsPayload.__name__])
|
||||
@console_ns.response(200, "Agent Skill bindings replaced", console_ns.models[AgentSkillBindingsResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def put(self, current_tenant_id: str, current_user: Account, agent_id: str):
|
||||
try:
|
||||
payload = AgentSkillBindingsPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().replace_agent_bindings(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
agent_id=agent_id,
|
||||
skill_ids=payload.skill_ids,
|
||||
)
|
||||
return dump_response(AgentSkillBindingsResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"WorkspaceAgentSkillBindingsApi",
|
||||
"WorkspaceSkillApi",
|
||||
"WorkspaceSkillDuplicateApi",
|
||||
"WorkspaceSkillExportApi",
|
||||
"WorkspaceSkillFilesApi",
|
||||
"WorkspaceSkillImportApi",
|
||||
"WorkspaceSkillPublishApi",
|
||||
"WorkspaceSkillReferencesApi",
|
||||
"WorkspaceSkillRestoreApi",
|
||||
"WorkspaceSkillTagsApi",
|
||||
"WorkspaceSkillVersionApi",
|
||||
"WorkspaceSkillVersionsApi",
|
||||
"WorkspaceSkillsApi",
|
||||
]
|
||||
@@ -37,6 +37,7 @@ from controllers.console.wraps import (
|
||||
with_current_user,
|
||||
)
|
||||
from enums.cloud_plan import CloudPlan
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import dump_response, to_timestamp
|
||||
@@ -233,7 +234,7 @@ class TenantListApi(Resource):
|
||||
tenants = [tenant for tenant, _ in tenant_rows]
|
||||
tenant_dicts = []
|
||||
is_enterprise_only = dify_config.ENTERPRISE_ENABLED and not dify_config.BILLING_ENABLED
|
||||
is_saas = dify_config.EDITION == "CLOUD" and dify_config.BILLING_ENABLED
|
||||
is_saas = dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and dify_config.BILLING_ENABLED
|
||||
tenant_plans: dict[str, SubscriptionPlan] = {}
|
||||
|
||||
if is_saas:
|
||||
|
||||
@@ -20,6 +20,7 @@ from controllers.common.wraps import (
|
||||
from controllers.console.auth.error import AuthenticationFailedError, EmailCodeError
|
||||
from controllers.console.workspace.error import AccountNotInitializedError
|
||||
from enums.cloud_plan import CloudPlan
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from libs.encryption import FieldEncryption
|
||||
@@ -129,7 +130,7 @@ def account_initialization_required[R](view: Callable[..., R]) -> Callable[...,
|
||||
def only_edition_cloud[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
@wraps(view)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||
if dify_config.EDITION != "CLOUD":
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||
abort(404)
|
||||
|
||||
return view(*args, **kwargs)
|
||||
@@ -151,7 +152,7 @@ def only_edition_enterprise[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
def only_edition_self_hosted[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
@wraps(view)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||
if dify_config.EDITION != "SELF_HOSTED":
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||
abort(404)
|
||||
|
||||
return view(*args, **kwargs)
|
||||
@@ -327,7 +328,7 @@ def setup_required[R](view: Callable[..., R]) -> Callable[..., R]:
|
||||
# The overloads keep Resource methods method-aware for pyrefly while
|
||||
# preserving support for plain functions used in tests and utilities.
|
||||
# check setup
|
||||
if dify_config.EDITION == "SELF_HOSTED" and not _is_setup_completed():
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD and not _is_setup_completed():
|
||||
if os.environ.get("INIT_PASSWORD"):
|
||||
raise NotInitValidateError()
|
||||
raise NotSetupError()
|
||||
|
||||
@@ -23,6 +23,7 @@ from .knowledge import retrieval as _knowledge_retrieval
|
||||
from .plugin import agent_config as _agent_config
|
||||
from .plugin import agent_drive as _agent_drive
|
||||
from .plugin import plugin as _plugin
|
||||
from .plugin import skills as _skills
|
||||
from .workspace import workspace as _workspace
|
||||
|
||||
api.add_namespace(inner_api_ns)
|
||||
@@ -36,6 +37,7 @@ __all__ = [
|
||||
"_mail",
|
||||
"_plugin",
|
||||
"_runtime_credentials",
|
||||
"_skills",
|
||||
"_workspace",
|
||||
"api",
|
||||
"bp",
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Inner API for published workspace Skills.
|
||||
|
||||
These endpoints are called by trusted runtime services. They expose only
|
||||
published Skill artifacts, never draft files or editable metadata.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
|
||||
from flask import request, send_file
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from controllers.console.wraps import setup_required
|
||||
from controllers.inner_api import inner_api_ns
|
||||
from controllers.inner_api.wraps import plugin_inner_api_only
|
||||
from services.skill_management_service import SkillManagementService, SkillManagementServiceError
|
||||
|
||||
|
||||
class _SkillTargetQuery(BaseModel):
|
||||
tenant_id: str
|
||||
|
||||
|
||||
def _target_query_from_request() -> _SkillTargetQuery:
|
||||
return _SkillTargetQuery.model_validate({"tenant_id": request.args.get("tenant_id")})
|
||||
|
||||
|
||||
def _error_response(exc: SkillManagementServiceError) -> tuple[dict[str, str], int]:
|
||||
return {"code": exc.code, "message": exc.message}, exc.status_code
|
||||
|
||||
|
||||
@inner_api_ns.route("/skills/<string:skill_id>/pull")
|
||||
class PublishedSkillPullApi(Resource):
|
||||
@setup_required
|
||||
@plugin_inner_api_only
|
||||
@inner_api_ns.doc("published_skill_pull")
|
||||
def get(self, skill_id: str):
|
||||
try:
|
||||
query = _target_query_from_request()
|
||||
result = SkillManagementService().pull_published_archive(tenant_id=query.tenant_id, skill_id=skill_id)
|
||||
return send_file(
|
||||
io.BytesIO(result.payload),
|
||||
mimetype=result.mime_type,
|
||||
as_attachment=True,
|
||||
download_name=result.filename,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
__all__ = ["PublishedSkillPullApi"]
|
||||
@@ -8,6 +8,7 @@ from werkzeug.exceptions import InternalServerError
|
||||
|
||||
from configs import dify_config
|
||||
from core.rbac import RBACPermission, RBACResourceScope
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from libs.oauth_bearer import Scope, TokenType
|
||||
from models.account import Account, Tenant, TenantAccountRole
|
||||
from models.model import App, EndUser
|
||||
@@ -26,7 +27,7 @@ class CallerKind(StrEnum):
|
||||
|
||||
|
||||
def current_edition() -> Edition:
|
||||
if dify_config.EDITION == "CLOUD":
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||
return Edition.SAAS
|
||||
if dify_config.ENTERPRISE_ENABLED:
|
||||
return Edition.EE
|
||||
|
||||
@@ -2,6 +2,7 @@ from flask_restx import Resource
|
||||
|
||||
from controllers.common.schema import register_response_schema_models
|
||||
from controllers.web import web_ns
|
||||
from libs.helper import dump_response
|
||||
from services.feature_service import FeatureService, SystemFeatureModel
|
||||
|
||||
register_response_schema_models(web_ns, SystemFeatureModel)
|
||||
@@ -10,7 +11,10 @@ register_response_schema_models(web_ns, SystemFeatureModel)
|
||||
@web_ns.route("/system-features")
|
||||
class SystemFeatureApi(Resource):
|
||||
@web_ns.doc("get_system_features")
|
||||
@web_ns.doc(description="Get system feature flags and configuration")
|
||||
@web_ns.doc(
|
||||
description="Get the non-sensitive bootstrap snapshot exposed before Console or Web authentication. "
|
||||
"This is not a general feature registry."
|
||||
)
|
||||
@web_ns.doc(responses={200: "System features retrieved successfully", 500: "Internal server error"})
|
||||
@web_ns.response(
|
||||
200,
|
||||
@@ -18,22 +22,11 @@ class SystemFeatureApi(Resource):
|
||||
web_ns.models[SystemFeatureModel.__name__],
|
||||
)
|
||||
def get(self):
|
||||
"""Get system feature flags and configuration.
|
||||
|
||||
Returns the current system feature flags and configuration
|
||||
that control various functionalities across the platform.
|
||||
|
||||
Returns:
|
||||
dict: System feature configuration object
|
||||
"""Get the non-sensitive bootstrap snapshot exposed before authentication.
|
||||
|
||||
This endpoint is akin to the `SystemFeatureApi` endpoint in api/controllers/console/feature.py,
|
||||
except it is intended for use by the web app, instead of the console dashboard.
|
||||
|
||||
NOTE: This endpoint is unauthenticated by design, as it provides system features
|
||||
data required for webapp initialization.
|
||||
|
||||
Authentication would create circular dependency (can't authenticate without webapp loading).
|
||||
|
||||
Only non-sensitive configuration data should be returned by this endpoint.
|
||||
Authentication configuration must be available before the authentication flow can be selected.
|
||||
"""
|
||||
return FeatureService.get_system_features().model_dump()
|
||||
return dump_response(SystemFeatureModel, FeatureService.get_system_features())
|
||||
|
||||
@@ -24,7 +24,7 @@ from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import RateLimiter, dump_response, extract_remote_ip, to_timestamp
|
||||
from models.account import TenantStatus
|
||||
from models.model import App, Site
|
||||
from models.model import App, AppMode, Site
|
||||
from repositories.factory import DifyAPIRepositoryFactory
|
||||
from services.feature_service import FeatureService
|
||||
from services.human_input_file_upload_service import HumanInputFileUploadService
|
||||
@@ -207,6 +207,7 @@ class HumanInputFormApi(Resource):
|
||||
site=WebAppSiteResponse.from_app_site(
|
||||
tenant=tenant,
|
||||
app_model=app_model,
|
||||
mode=AppMode.value_of(app_model.mode),
|
||||
site=site,
|
||||
end_user_id=None,
|
||||
features=features,
|
||||
|
||||
@@ -8,12 +8,13 @@ from configs import dify_config
|
||||
from controllers.common.schema import register_response_schema_models
|
||||
from controllers.web import web_ns
|
||||
from controllers.web.wraps import WebApiResource
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from extensions.ext_database import db
|
||||
from extensions.storage.storage_type import StorageType
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import build_icon_url
|
||||
from models.account import Tenant, TenantStatus
|
||||
from models.model import App, EndUser, IconType, Site
|
||||
from models.model import App, AppMode, EndUser, IconType, Site
|
||||
from services.feature_service import FeatureModel, FeatureService
|
||||
from services.file_service import FileService
|
||||
|
||||
@@ -66,6 +67,7 @@ class WebAppCustomConfigResponse(ResponseModel):
|
||||
|
||||
class WebAppSiteResponse(ResponseModel):
|
||||
app_id: str
|
||||
mode: AppMode
|
||||
end_user_id: str | None = None
|
||||
enable_site: bool
|
||||
site: WebSiteResponse
|
||||
@@ -82,6 +84,7 @@ class WebAppSiteResponse(ResponseModel):
|
||||
*,
|
||||
tenant: Tenant,
|
||||
app_model: App,
|
||||
mode: AppMode,
|
||||
site: Site,
|
||||
end_user_id: str | None,
|
||||
features: FeatureModel,
|
||||
@@ -108,6 +111,7 @@ class WebAppSiteResponse(ResponseModel):
|
||||
|
||||
return cls(
|
||||
app_id=app_model.id,
|
||||
mode=mode,
|
||||
end_user_id=end_user_id,
|
||||
enable_site=app_model.enable_site,
|
||||
site=site_response,
|
||||
@@ -127,7 +131,9 @@ def _build_site_icon_url(*, site: Site, tenant_id: str) -> str | None:
|
||||
"""Use direct S3 URLs only in Cloud Mode and preserve preview URLs elsewhere."""
|
||||
if site.icon_type != IconType.IMAGE or not site.icon:
|
||||
return None
|
||||
if dify_config.EDITION == "CLOUD" and StorageType(dify_config.STORAGE_TYPE) == StorageType.S3:
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and (
|
||||
StorageType(dify_config.STORAGE_TYPE) == StorageType.S3
|
||||
):
|
||||
return FileService(db.engine).get_file_presigned_url(file_id=site.icon, tenant_id=tenant_id)
|
||||
return build_icon_url(site.icon_type, site.icon)
|
||||
|
||||
@@ -164,6 +170,7 @@ class AppSiteApi(WebApiResource):
|
||||
return WebAppSiteResponse.from_app_site(
|
||||
tenant=tenant,
|
||||
app_model=app_model,
|
||||
mode=AppMode.value_of(app_model.mode_compatible_with_agent_with_session(session=db.session())),
|
||||
site=site,
|
||||
end_user_id=end_user.id,
|
||||
features=features,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Publication visibility rules for calling roster Agents from Workflows.
|
||||
|
||||
``Agent.active_config_is_published`` describes whether the editable shared
|
||||
draft still matches the active snapshot. It is false both before the first
|
||||
publish and after a published Agent receives new draft edits, so it must not be
|
||||
used as a runtime availability flag. App-backed Agents are callable from a
|
||||
Workflow only when the active snapshot has a revision created by a
|
||||
publish-visible operation. Direct roster Agents are publish-visible by
|
||||
construction and only need an active snapshot.
|
||||
"""
|
||||
|
||||
from sqlalchemy import and_, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.sql.elements import ColumnElement
|
||||
|
||||
from models.agent import Agent, AgentConfigRevision, AgentConfigRevisionOperation, AgentScope, AgentSource
|
||||
|
||||
PUBLISH_VISIBLE_APP_BACKED_REVISION_OPERATIONS = frozenset(
|
||||
{
|
||||
AgentConfigRevisionOperation.PUBLISH_DRAFT,
|
||||
AgentConfigRevisionOperation.SAVE_CURRENT_VERSION,
|
||||
AgentConfigRevisionOperation.SAVE_NEW_VERSION,
|
||||
AgentConfigRevisionOperation.SAVE_NEW_AGENT,
|
||||
AgentConfigRevisionOperation.SAVE_TO_ROSTER,
|
||||
AgentConfigRevisionOperation.RESTORE_VERSION,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def workflow_callable_active_snapshot_filter() -> ColumnElement[bool]:
|
||||
"""Return the SQL predicate for an Agent with a Workflow-callable active snapshot.
|
||||
|
||||
The caller remains responsible for tenant, roster scope, lifecycle status,
|
||||
and model configuration filters. The correlated revision lookup makes the
|
||||
predicate safe to compose into roster pagination queries.
|
||||
"""
|
||||
|
||||
app_backed_agent = or_(
|
||||
Agent.source == AgentSource.AGENT_APP,
|
||||
and_(
|
||||
Agent.source == AgentSource.IMPORTED,
|
||||
Agent.scope == AgentScope.ROSTER,
|
||||
Agent.app_id.is_not(None),
|
||||
),
|
||||
)
|
||||
publish_visible_revision_exists = (
|
||||
select(AgentConfigRevision.id)
|
||||
.where(
|
||||
AgentConfigRevision.tenant_id == Agent.tenant_id,
|
||||
AgentConfigRevision.agent_id == Agent.id,
|
||||
AgentConfigRevision.current_snapshot_id == Agent.active_config_snapshot_id,
|
||||
AgentConfigRevision.operation.in_(PUBLISH_VISIBLE_APP_BACKED_REVISION_OPERATIONS),
|
||||
)
|
||||
.correlate(Agent)
|
||||
.exists()
|
||||
)
|
||||
return and_(
|
||||
Agent.active_config_snapshot_id.is_not(None),
|
||||
or_(
|
||||
~app_backed_agent,
|
||||
publish_visible_revision_exists,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def agent_has_workflow_callable_active_snapshot(*, session: Session, agent: Agent) -> bool:
|
||||
"""Return whether ``agent`` has an active snapshot visible to Workflow.
|
||||
|
||||
This object-level form is useful after ownership and lifecycle checks have
|
||||
already loaded an Agent. It intentionally ignores dirty draft state so a
|
||||
previously published snapshot keeps serving while later edits remain
|
||||
unpublished.
|
||||
"""
|
||||
|
||||
if not agent.active_config_snapshot_id:
|
||||
return False
|
||||
is_app_backed = agent.source == AgentSource.AGENT_APP or (
|
||||
agent.source == AgentSource.IMPORTED and agent.scope == AgentScope.ROSTER and agent.app_id is not None
|
||||
)
|
||||
if not is_app_backed:
|
||||
return True
|
||||
return bool(
|
||||
session.scalar(
|
||||
select(AgentConfigRevision.id)
|
||||
.where(
|
||||
AgentConfigRevision.tenant_id == agent.tenant_id,
|
||||
AgentConfigRevision.agent_id == agent.id,
|
||||
AgentConfigRevision.current_snapshot_id == agent.active_config_snapshot_id,
|
||||
AgentConfigRevision.operation.in_(PUBLISH_VISIBLE_APP_BACKED_REVISION_OPERATIONS),
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
)
|
||||
@@ -616,23 +616,34 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||
message_snapshot = MessageSnapshot.from_message(message)
|
||||
session.close()
|
||||
|
||||
# return response or stream generator
|
||||
response = self._handle_advanced_chat_response(
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow=workflow_snapshot,
|
||||
queue_manager=queue_manager,
|
||||
conversation=conversation_snapshot,
|
||||
message=message_snapshot,
|
||||
user=user,
|
||||
stream=stream,
|
||||
draft_var_saver_factory=self._get_draft_var_saver_factory(
|
||||
invoke_from,
|
||||
account=user,
|
||||
tenant_id=application_generate_entity.app_config.tenant_id,
|
||||
),
|
||||
)
|
||||
try:
|
||||
response = self._handle_advanced_chat_response(
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow=workflow_snapshot,
|
||||
queue_manager=queue_manager,
|
||||
conversation=conversation_snapshot,
|
||||
message=message_snapshot,
|
||||
user=user,
|
||||
stream=stream,
|
||||
draft_var_saver_factory=self._get_draft_var_saver_factory(
|
||||
invoke_from,
|
||||
account=user,
|
||||
tenant_id=application_generate_entity.app_config.tenant_id,
|
||||
),
|
||||
)
|
||||
converted_response = AdvancedChatAppGenerateResponseConverter.convert(
|
||||
response=response,
|
||||
invoke_from=invoke_from,
|
||||
)
|
||||
except BaseException:
|
||||
self._join_worker_thread(worker_thread)
|
||||
raise
|
||||
|
||||
return AdvancedChatAppGenerateResponseConverter.convert(response=response, invoke_from=invoke_from)
|
||||
if isinstance(converted_response, Generator):
|
||||
return self._wrap_stream_with_worker_thread_join(converted_response, worker_thread)
|
||||
|
||||
self._join_worker_thread(worker_thread)
|
||||
return converted_response
|
||||
|
||||
def _generate_worker(
|
||||
self,
|
||||
|
||||
@@ -538,6 +538,7 @@ class AgentAppGenerator(MessageBasedAppGenerator):
|
||||
request_builder=AgentAppRuntimeRequestBuilder(credentials_provider=credentials_provider),
|
||||
agent_backend_client=create_agent_backend_run_client(
|
||||
base_url=dify_config.AGENT_BACKEND_BASE_URL,
|
||||
api_token=dify_config.AGENT_BACKEND_API_TOKEN,
|
||||
use_fake=dify_config.AGENT_BACKEND_USE_FAKE,
|
||||
fake_scenario=dify_config.AGENT_BACKEND_FAKE_SCENARIO,
|
||||
stream_read_timeout_seconds=dify_config.AGENT_BACKEND_STREAM_READ_TIMEOUT_SECONDS,
|
||||
|
||||
@@ -43,6 +43,7 @@ from core.workflow.nodes.agent_v2.runtime_request_builder import (
|
||||
build_config_layer_config,
|
||||
build_knowledge_layer_config,
|
||||
build_shell_layer_config,
|
||||
load_runtime_agent_skill_configs,
|
||||
)
|
||||
from models.agent_config_entities import AgentSoulConfig, AgentSoulToolsConfig
|
||||
from models.provider_ids import ModelProviderID
|
||||
@@ -125,14 +126,22 @@ class AgentAppRuntimeRequestBuilder:
|
||||
"cli_tool_count": len(agent_soul.tools.cli_tools),
|
||||
}
|
||||
|
||||
runtime_config_skills = load_runtime_agent_skill_configs(
|
||||
tenant_id=context.dify_context.tenant_id,
|
||||
agent_id=context.agent_id,
|
||||
)
|
||||
config_layer_config, config_warnings = build_config_layer_config(
|
||||
agent_soul,
|
||||
agent_id=context.agent_id,
|
||||
config_version_id=context.agent_config_snapshot_id,
|
||||
config_version_kind=context.agent_config_version_kind,
|
||||
runtime_config_skills=runtime_config_skills,
|
||||
)
|
||||
append_runtime_warnings(metadata, config_warnings)
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(
|
||||
agent_soul,
|
||||
runtime_config_skills=runtime_config_skills,
|
||||
)
|
||||
knowledge_config = build_knowledge_layer_config(agent_soul)
|
||||
|
||||
request = self._request_builder.build_for_agent_app(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
import threading
|
||||
from collections.abc import Generator, Mapping, Sequence
|
||||
from contextlib import AbstractContextManager, nullcontext
|
||||
from typing import TYPE_CHECKING, Any, Union, final
|
||||
@@ -23,6 +25,10 @@ from services.workflow_draft_variable_service import DraftVariableSaver as Draft
|
||||
if TYPE_CHECKING:
|
||||
from graphon.variables.input_entities import VariableEntity
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_WORKER_THREAD_JOIN_TIMEOUT_SECONDS = 300
|
||||
|
||||
|
||||
@final
|
||||
class _DebuggerDraftVariableSaver:
|
||||
@@ -64,6 +70,29 @@ class _DebuggerDraftVariableSaver:
|
||||
class BaseAppGenerator:
|
||||
_file_access_controller: DatabaseFileAccessController = DatabaseFileAccessController()
|
||||
|
||||
@staticmethod
|
||||
def _join_worker_thread(worker_thread: threading.Thread) -> None:
|
||||
# Bound the wait so a leaked app worker cannot occupy an execution slot indefinitely.
|
||||
worker_thread.join(timeout=_WORKER_THREAD_JOIN_TIMEOUT_SECONDS)
|
||||
if worker_thread.is_alive():
|
||||
logger.warning(
|
||||
"Possible app worker thread leak: thread_name=%s timeout_seconds=%s; "
|
||||
"continuing without waiting further to avoid occupying an execution slot indefinitely",
|
||||
worker_thread.name,
|
||||
_WORKER_THREAD_JOIN_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _wrap_stream_with_worker_thread_join[ResponseT](
|
||||
response_stream: Generator[ResponseT, None, None],
|
||||
worker_thread: threading.Thread,
|
||||
) -> Generator[ResponseT, None, None]:
|
||||
"""Keep the producer owned by the response stream until both finish."""
|
||||
try:
|
||||
yield from response_stream
|
||||
finally:
|
||||
BaseAppGenerator._join_worker_thread(worker_thread)
|
||||
|
||||
@staticmethod
|
||||
def _bind_file_access_scope(
|
||||
*,
|
||||
|
||||
@@ -351,17 +351,28 @@ class PipelineGenerator(BaseAppGenerator):
|
||||
user,
|
||||
tenant_id=pipeline.tenant_id,
|
||||
)
|
||||
# return response or stream generator
|
||||
response = self._handle_response(
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow=workflow,
|
||||
queue_manager=queue_manager,
|
||||
user=user,
|
||||
stream=streaming,
|
||||
draft_var_saver_factory=draft_var_saver_factory,
|
||||
)
|
||||
try:
|
||||
response = self._handle_response(
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow=workflow,
|
||||
queue_manager=queue_manager,
|
||||
user=user,
|
||||
stream=streaming,
|
||||
draft_var_saver_factory=draft_var_saver_factory,
|
||||
)
|
||||
converted_response = WorkflowAppGenerateResponseConverter.convert(
|
||||
response=response,
|
||||
invoke_from=invoke_from,
|
||||
)
|
||||
except BaseException:
|
||||
self._join_worker_thread(worker_thread)
|
||||
raise
|
||||
|
||||
return WorkflowAppGenerateResponseConverter.convert(response=response, invoke_from=invoke_from)
|
||||
if isinstance(converted_response, Generator):
|
||||
return self._wrap_stream_with_worker_thread_join(converted_response, worker_thread)
|
||||
|
||||
self._join_worker_thread(worker_thread)
|
||||
return converted_response
|
||||
|
||||
def single_iteration_generate(
|
||||
self,
|
||||
|
||||
@@ -405,17 +405,28 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
tenant_id=app_model.tenant_id,
|
||||
)
|
||||
|
||||
# return response or stream generator
|
||||
response = self._handle_response(
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow=workflow,
|
||||
queue_manager=queue_manager,
|
||||
user=user,
|
||||
draft_var_saver_factory=draft_var_saver_factory,
|
||||
stream=streaming,
|
||||
)
|
||||
try:
|
||||
response = self._handle_response(
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow=workflow,
|
||||
queue_manager=queue_manager,
|
||||
user=user,
|
||||
draft_var_saver_factory=draft_var_saver_factory,
|
||||
stream=streaming,
|
||||
)
|
||||
converted_response = WorkflowAppGenerateResponseConverter.convert(
|
||||
response=response,
|
||||
invoke_from=invoke_from,
|
||||
)
|
||||
except BaseException:
|
||||
self._join_worker_thread(worker_thread)
|
||||
raise
|
||||
|
||||
return WorkflowAppGenerateResponseConverter.convert(response=response, invoke_from=invoke_from)
|
||||
if isinstance(converted_response, Generator):
|
||||
return self._wrap_stream_with_worker_thread_join(converted_response, worker_thread)
|
||||
|
||||
self._join_worker_thread(worker_thread)
|
||||
return converted_response
|
||||
|
||||
def single_iteration_generate(
|
||||
self,
|
||||
|
||||
@@ -39,15 +39,16 @@ class Jinja2TemplateTransformer(TemplateTransformer):
|
||||
@override
|
||||
def get_runner_script(cls) -> str:
|
||||
runner_script = dedent(f"""
|
||||
import jinja2
|
||||
import json
|
||||
from base64 import b64decode
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
# declare main function
|
||||
def main(**inputs):
|
||||
# Decode base64-encoded template to handle special characters safely
|
||||
template_code = b64decode('{cls._template_b64_placeholder}').decode('utf-8')
|
||||
template = jinja2.Template(template_code)
|
||||
env = SandboxedEnvironment()
|
||||
template = env.from_string(template_code)
|
||||
return template.render(**inputs)
|
||||
|
||||
# decode and prepare input dict
|
||||
@@ -67,12 +68,13 @@ class Jinja2TemplateTransformer(TemplateTransformer):
|
||||
@override
|
||||
def get_preload_script(cls) -> str:
|
||||
preload_script = dedent("""
|
||||
import jinja2
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
from base64 import b64decode
|
||||
|
||||
def _jinja2_preload_():
|
||||
# prepare jinja2 environment, load template and render before to avoid sandbox issue
|
||||
template = jinja2.Template('{{s}}')
|
||||
# prepare jinja2 sandboxed environment, load template and render
|
||||
env = SandboxedEnvironment()
|
||||
template = env.from_string('{{s}}')
|
||||
template.render(s='a')
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -71,7 +71,7 @@ def check_credential_policy_compliance(
|
||||
)
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
if not FeatureService.get_system_features().plugin_manager.enabled or not credential_id:
|
||||
if not FeatureService.is_plugin_manager_enabled() or not credential_id:
|
||||
return
|
||||
|
||||
# Check if credential exists in database first (if requested)
|
||||
|
||||
@@ -6,6 +6,7 @@ from pydantic import BaseModel
|
||||
from configs import dify_config
|
||||
from core.entities import DEFAULT_PLUGIN_ID
|
||||
from core.entities.provider_entities import ProviderQuotaType, QuotaUnit, RestrictModel
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from graphon.model_runtime.entities.model_entities import ModelType
|
||||
|
||||
|
||||
@@ -49,7 +50,7 @@ class HostingConfiguration:
|
||||
self.moderation_config = None
|
||||
|
||||
def init_app(self, app: Flask):
|
||||
if dify_config.EDITION != "CLOUD":
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||
return
|
||||
|
||||
self.provider_map[f"{DEFAULT_PLUGIN_ID}/azure_openai/azure_openai"] = self.init_azure_openai()
|
||||
|
||||
@@ -519,7 +519,7 @@ class IndexingRunner:
|
||||
def filter_string(text):
|
||||
text = re.sub(r"<\|", "<", text)
|
||||
text = re.sub(r"\|>", ">", text)
|
||||
text = re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\xEF\xBF\xBE]", "", text)
|
||||
text = re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "", text)
|
||||
# Unicode U+FFFE
|
||||
text = re.sub("\ufffe", "", text)
|
||||
return text
|
||||
|
||||
@@ -66,7 +66,7 @@ from services.enterprise.plugin_manager_service import (
|
||||
PreUninstallPluginRequest,
|
||||
)
|
||||
from services.errors.plugin import PluginInstallationForbiddenError
|
||||
from services.feature_service import FeatureService, PluginInstallationScope
|
||||
from services.feature_service import FeatureService, PluginInstallationPermissionModel, PluginInstallationScope
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_provider_entities_adapter: TypeAdapter[list[ProviderEntity]] = TypeAdapter(list[ProviderEntity])
|
||||
@@ -434,14 +434,18 @@ class PluginService:
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _fetch_plugin_model_providers_uncached(
|
||||
cls, tenant_id: str, client: PluginModelClient | None
|
||||
) -> tuple[ProviderEntity, ...]:
|
||||
model_client = client or PluginModelClient()
|
||||
return tuple(cls._to_provider_entity(provider) for provider in model_client.fetch_model_providers(tenant_id))
|
||||
|
||||
@classmethod
|
||||
def _fetch_and_cache_plugin_model_providers(
|
||||
cls, tenant_id: str, client: PluginModelClient | None, *, refresh_generation: int | None
|
||||
) -> tuple[ProviderEntity, ...]:
|
||||
model_client = client or PluginModelClient()
|
||||
providers = tuple(
|
||||
cls._to_provider_entity(provider) for provider in model_client.fetch_model_providers(tenant_id)
|
||||
)
|
||||
providers = cls._fetch_plugin_model_providers_uncached(tenant_id, client)
|
||||
generation = cls._load_plugin_model_providers_generation(tenant_id)
|
||||
if generation is not None and generation == refresh_generation:
|
||||
cls._store_cached_plugin_model_providers(tenant_id, generation, providers)
|
||||
@@ -471,6 +475,9 @@ class PluginService:
|
||||
are intentionally owned by this service so tenant isolation and cache
|
||||
expiry are handled in one place.
|
||||
"""
|
||||
if not dify_config.PLUGIN_MODEL_PROVIDERS_CACHE_ENABLED:
|
||||
return cls._fetch_plugin_model_providers_uncached(tenant_id, client)
|
||||
|
||||
deadline = time.monotonic() + cls.PLUGIN_MODEL_PROVIDERS_LOCK_WAIT_TIMEOUT
|
||||
|
||||
while True:
|
||||
@@ -597,22 +604,30 @@ class PluginService:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _check_marketplace_only_permission():
|
||||
def _check_marketplace_only_permission() -> None:
|
||||
"""
|
||||
Check if the marketplace only permission is enabled
|
||||
"""
|
||||
features = FeatureService.get_system_features()
|
||||
if features.plugin_installation_permission.restrict_to_marketplace_only:
|
||||
permission = PluginService._get_plugin_installation_permission()
|
||||
if permission.restrict_to_marketplace_only:
|
||||
raise PluginInstallationForbiddenError("Plugin installation is restricted to marketplace only")
|
||||
|
||||
@staticmethod
|
||||
def _check_plugin_installation_scope(plugin_verification: PluginVerification | None):
|
||||
def _get_plugin_installation_permission() -> PluginInstallationPermissionModel:
|
||||
"""Resolve the validated policy and reject deny-all before any installation side effect."""
|
||||
permission = FeatureService.get_plugin_installation_permission()
|
||||
if permission.plugin_installation_scope == PluginInstallationScope.NONE:
|
||||
raise PluginInstallationForbiddenError("Installing plugins is not allowed")
|
||||
return permission
|
||||
|
||||
@staticmethod
|
||||
def _check_plugin_installation_scope(plugin_verification: PluginVerification | None) -> None:
|
||||
"""
|
||||
Check the plugin installation scope
|
||||
"""
|
||||
features = FeatureService.get_system_features()
|
||||
permission = PluginService._get_plugin_installation_permission()
|
||||
|
||||
match features.plugin_installation_permission.plugin_installation_scope:
|
||||
match permission.plugin_installation_scope:
|
||||
case PluginInstallationScope.OFFICIAL_ONLY:
|
||||
if (
|
||||
plugin_verification is None
|
||||
@@ -627,10 +642,10 @@ class PluginService:
|
||||
raise PluginInstallationForbiddenError(
|
||||
"Plugin installation is restricted to official and specific partners"
|
||||
)
|
||||
case PluginInstallationScope.NONE:
|
||||
raise PluginInstallationForbiddenError("Installing plugins is not allowed")
|
||||
case PluginInstallationScope.ALL:
|
||||
pass
|
||||
case _:
|
||||
raise PluginInstallationForbiddenError("Plugin installation policy is invalid")
|
||||
|
||||
@staticmethod
|
||||
def get_debugging_key(tenant_id: str) -> str:
|
||||
@@ -900,7 +915,7 @@ class PluginService:
|
||||
# check if plugin pkg is already downloaded
|
||||
manager = PluginInstaller()
|
||||
|
||||
features = FeatureService.get_system_features()
|
||||
permission = PluginService._get_plugin_installation_permission()
|
||||
|
||||
try:
|
||||
manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier)
|
||||
@@ -912,7 +927,7 @@ class PluginService:
|
||||
response = manager.upload_pkg(
|
||||
tenant_id,
|
||||
pkg,
|
||||
verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
|
||||
verify_signature=permission.restrict_to_marketplace_only,
|
||||
)
|
||||
|
||||
# check if the plugin is available to install
|
||||
@@ -967,11 +982,11 @@ class PluginService:
|
||||
"""
|
||||
PluginService._check_marketplace_only_permission()
|
||||
manager = PluginInstaller()
|
||||
features = FeatureService.get_system_features()
|
||||
permission = PluginService._get_plugin_installation_permission()
|
||||
response = manager.upload_pkg(
|
||||
tenant_id,
|
||||
pkg,
|
||||
verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
|
||||
verify_signature=permission.restrict_to_marketplace_only,
|
||||
)
|
||||
PluginService._check_plugin_installation_scope(response.verification)
|
||||
|
||||
@@ -989,13 +1004,13 @@ class PluginService:
|
||||
pkg = download_with_size_limit(
|
||||
f"https://github.com/{repo}/releases/download/{version}/{package}", dify_config.PLUGIN_MAX_PACKAGE_SIZE
|
||||
)
|
||||
features = FeatureService.get_system_features()
|
||||
permission = PluginService._get_plugin_installation_permission()
|
||||
|
||||
manager = PluginInstaller()
|
||||
response = manager.upload_pkg(
|
||||
tenant_id,
|
||||
pkg,
|
||||
verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
|
||||
verify_signature=permission.restrict_to_marketplace_only,
|
||||
)
|
||||
PluginService._check_plugin_installation_scope(response.verification)
|
||||
|
||||
@@ -1069,7 +1084,7 @@ class PluginService:
|
||||
if not dify_config.MARKETPLACE_ENABLED:
|
||||
raise ValueError("marketplace is not enabled")
|
||||
|
||||
features = FeatureService.get_system_features()
|
||||
permission = PluginService._get_plugin_installation_permission()
|
||||
|
||||
manager = PluginInstaller()
|
||||
try:
|
||||
@@ -1079,7 +1094,7 @@ class PluginService:
|
||||
response = manager.upload_pkg(
|
||||
tenant_id,
|
||||
pkg,
|
||||
verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
|
||||
verify_signature=permission.restrict_to_marketplace_only,
|
||||
)
|
||||
# check if the plugin is available to install
|
||||
PluginService._check_plugin_installation_scope(response.verification)
|
||||
@@ -1101,7 +1116,7 @@ class PluginService:
|
||||
# collect actual plugin_unique_identifiers
|
||||
actual_plugin_unique_identifiers = []
|
||||
metas = []
|
||||
features = FeatureService.get_system_features()
|
||||
permission = PluginService._get_plugin_installation_permission()
|
||||
|
||||
# check if already downloaded
|
||||
for plugin_unique_identifier in plugin_unique_identifiers:
|
||||
@@ -1119,7 +1134,7 @@ class PluginService:
|
||||
response = manager.upload_pkg(
|
||||
tenant_id,
|
||||
pkg,
|
||||
verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
|
||||
verify_signature=permission.restrict_to_marketplace_only,
|
||||
)
|
||||
# check if the plugin is available to install
|
||||
PluginService._check_plugin_installation_scope(response.verification)
|
||||
|
||||
@@ -34,6 +34,7 @@ from core.entities.provider_entities import (
|
||||
from core.helper import encrypter
|
||||
from core.helper.model_provider_cache import ProviderCredentialsCache, ProviderCredentialsCacheType
|
||||
from core.helper.position_helper import is_filtered
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from extensions import ext_hosting_provider
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
@@ -743,7 +744,7 @@ class ProviderManager:
|
||||
|
||||
if preferred_provider_type_record:
|
||||
preferred_provider_type = preferred_provider_type_record.preferred_provider_type
|
||||
elif dify_config.EDITION == "CLOUD" and system_configuration.enabled:
|
||||
elif dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and system_configuration.enabled:
|
||||
preferred_provider_type = ProviderType.SYSTEM
|
||||
elif custom_configuration.provider or custom_configuration.models:
|
||||
preferred_provider_type = ProviderType.CUSTOM
|
||||
@@ -1538,7 +1539,7 @@ class ProviderManager:
|
||||
quota_type_to_provider_records_dict[provider_record.quota_type] = provider_record # type: ignore[index]
|
||||
quota_configurations = []
|
||||
|
||||
if dify_config.EDITION == "CLOUD":
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||
from services.credit_pool_service import CreditPoolService
|
||||
|
||||
trail_pool = CreditPoolService.get_pool(
|
||||
|
||||
@@ -9,7 +9,7 @@ class CleanProcessor:
|
||||
# remove invalid symbol
|
||||
text = re.sub(r"<\|", "<", text)
|
||||
text = re.sub(r"\|>", ">", text)
|
||||
text = re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\xEF\xBF\xBE]", "", text)
|
||||
text = re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "", text)
|
||||
# Unicode U+FFFE
|
||||
text = re.sub("\ufffe", "", text)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from core.rag.rerank.entity.weight import KeywordSetting, VectorSetting, Weights
|
||||
from core.rag.rerank.rerank_base import BaseRerankRunner
|
||||
from core.rag.rerank.rerank_factory import RerankRunnerFactory
|
||||
from core.rag.rerank.rerank_type import RerankMode
|
||||
from extensions.otel import trace_span
|
||||
from graphon.model_runtime.entities.model_entities import ModelType
|
||||
from graphon.model_runtime.errors.invoke import InvokeAuthorizationError
|
||||
|
||||
@@ -52,6 +53,7 @@ class DataPostProcessor:
|
||||
)
|
||||
self.reorder_runner = self._get_reorder_runner(reorder_enabled)
|
||||
|
||||
@trace_span()
|
||||
def invoke(
|
||||
self,
|
||||
query: str,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import concurrent.futures
|
||||
import functools
|
||||
import logging
|
||||
from collections.abc import Callable, Sequence
|
||||
from collections.abc import Sequence
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, NotRequired, TypedDict
|
||||
|
||||
from flask import Flask, current_app
|
||||
from opentelemetry import context as otel_context
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, load_only
|
||||
|
||||
@@ -26,7 +24,7 @@ from core.rag.rerank.rerank_type import RerankMode
|
||||
from core.rag.retrieval.retrieval_methods import RetrievalMethod
|
||||
from core.tools.signature import sign_upload_file_preview_url
|
||||
from extensions.ext_database import db
|
||||
from extensions.otel import trace_span
|
||||
from extensions.otel import propagate_context, trace_span
|
||||
from graphon.model_runtime.entities.model_entities import ModelType
|
||||
from models.dataset import (
|
||||
ChildChunk,
|
||||
@@ -92,20 +90,6 @@ default_retrieval_model: DefaultRetrievalModelDict = {
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _propagate_otel_context[**P, R](func: Callable[P, R]) -> Callable[P, R]:
|
||||
captured_context = otel_context.get_current()
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
token = otel_context.attach(captured_context)
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
otel_context.detach(token)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class RetrievalService:
|
||||
# Cache precompiled regular expressions to avoid repeated compilation
|
||||
@classmethod
|
||||
@@ -139,7 +123,7 @@ class RetrievalService:
|
||||
if query:
|
||||
futures.append(
|
||||
executor.submit(
|
||||
_propagate_otel_context(retrieval_service._retrieve),
|
||||
propagate_context(retrieval_service._retrieve),
|
||||
flask_app=current_app._get_current_object(), # type: ignore
|
||||
retrieval_method=retrieval_method,
|
||||
dataset=dataset,
|
||||
@@ -159,7 +143,7 @@ class RetrievalService:
|
||||
for attachment_id in attachment_ids:
|
||||
futures.append(
|
||||
executor.submit(
|
||||
_propagate_otel_context(retrieval_service._retrieve),
|
||||
propagate_context(retrieval_service._retrieve),
|
||||
flask_app=current_app._get_current_object(), # type: ignore
|
||||
retrieval_method=retrieval_method,
|
||||
dataset=dataset,
|
||||
@@ -820,7 +804,7 @@ class RetrievalService:
|
||||
if retrieval_method == RetrievalMethod.KEYWORD_SEARCH and query:
|
||||
futures.append(
|
||||
executor.submit(
|
||||
_propagate_otel_context(self.keyword_search),
|
||||
propagate_context(self.keyword_search),
|
||||
flask_app=current_app._get_current_object(), # type: ignore
|
||||
dataset_id=dataset.id,
|
||||
query=query,
|
||||
@@ -834,7 +818,7 @@ class RetrievalService:
|
||||
if query:
|
||||
futures.append(
|
||||
executor.submit(
|
||||
_propagate_otel_context(self.embedding_search),
|
||||
propagate_context(self.embedding_search),
|
||||
flask_app=current_app._get_current_object(), # type: ignore
|
||||
dataset_id=dataset.id,
|
||||
query=query,
|
||||
@@ -851,7 +835,7 @@ class RetrievalService:
|
||||
if attachment_id:
|
||||
futures.append(
|
||||
executor.submit(
|
||||
_propagate_otel_context(self.embedding_search),
|
||||
propagate_context(self.embedding_search),
|
||||
flask_app=current_app._get_current_object(), # type: ignore
|
||||
dataset_id=dataset.id,
|
||||
query=attachment_id,
|
||||
@@ -868,7 +852,7 @@ class RetrievalService:
|
||||
if RetrievalMethod.is_support_fulltext_search(retrieval_method) and query:
|
||||
futures.append(
|
||||
executor.submit(
|
||||
_propagate_otel_context(self.full_text_index_search),
|
||||
propagate_context(self.full_text_index_search),
|
||||
flask_app=current_app._get_current_object(), # type: ignore
|
||||
dataset_id=dataset.id,
|
||||
query=query,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user