diff --git a/web/context/i18n.ts b/web/context/i18n.ts index d57fc5b984..a10d9af513 100644 --- a/web/context/i18n.ts +++ b/web/context/i18n.ts @@ -1,4 +1,5 @@ import type { Locale } from '@/i18n-config/language' +import type { DocPathWithoutLang } from '@/types/doc-paths' import { useTranslation } from '#i18n' import { getDocLanguage, getLanguage, getPricingPageLanguage } from '@/i18n-config/language' @@ -19,12 +20,14 @@ export const useGetPricingPageLanguage = () => { } export const defaultDocBaseUrl = 'https://docs.dify.ai' -export const useDocLink = (baseUrl?: string): ((path?: string, pathMap?: { [index: string]: string }) => string) => { +export type DocPathMap = Partial> + +export const useDocLink = (baseUrl?: string): ((path?: DocPathWithoutLang, pathMap?: DocPathMap) => string) => { let baseDocUrl = baseUrl || defaultDocBaseUrl baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl const locale = useLocale() const docLanguage = getDocLanguage(locale) - return (path?: string, pathMap?: { [index: string]: string }): string => { + return (path?: DocPathWithoutLang, pathMap?: DocPathMap): string => { const pathUrl = path || '' let targetPath = (pathMap) ? pathMap[locale] || pathUrl : pathUrl targetPath = (targetPath.startsWith('/')) ? targetPath.slice(1) : targetPath diff --git a/web/eslint-rules/doc-redirects.js b/web/eslint-rules/doc-redirects.js new file mode 100644 index 0000000000..8891bcdfb9 --- /dev/null +++ b/web/eslint-rules/doc-redirects.js @@ -0,0 +1,70 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY +// +// Generated from: https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json +// Generated at: 2026-01-12T06:51:06.502Z + +/** @type {Map} */ +export const docRedirects = new Map([ + ['guides/knowledge-base/retrieval-test-and-citation', 'use-dify/knowledge/test-retrieval'], + ['use-dify/knowledge/retrieval-test-and-citation', 'use-dify/knowledge/test-retrieval'], + ['use-dify/build/variables#conversation-variables', 'use-dify/getting-started/key-concepts#variables'], + ['use-dify/build/variables#会话变量', 'use-dify/getting-started/key-concepts#变量'], + ['use-dify/build/variables#会話変数', 'use-dify/getting-started/key-concepts#変数'], + ['guides/workflow/variables#conversation-variables', 'use-dify/getting-started/key-concepts#variables'], + ['guides/workflow/variables#会话变量', 'use-dify/getting-started/key-concepts#变量'], + ['guides/workflow/variables#会話変数', 'use-dify/getting-started/key-concepts#変数'], + ['guides/workflow/node/start', 'use-dify/getting-started/key-concepts#workflow'], + ['guides/workflow/node/user-input', 'use-dify/nodes/user-input'], + ['guides/workflow/node/trigger', 'use-dify/nodes/trigger/overview'], + ['guides/workflow/node/schedule-trigger', 'use-dify/nodes/trigger/schedule-trigger'], + ['guides/workflow/node/webhook-trigger', 'use-dify/nodes/trigger/webhook-trigger'], + ['guides/workflow/node/plugin-trigger', 'use-dify/nodes/trigger/plugin-trigger'], + ['guides/workflow/node/knowledge-retrieval', 'use-dify/nodes/knowledge-retrieval'], + ['guides/workflow/node/agent', 'use-dify/nodes/agent'], + ['guides/workflow/node/question-classifier', 'use-dify/nodes/question-classifier'], + ['guides/workflow/node/ifelse', 'use-dify/nodes/ifelse'], + ['guides/workflow/node/iteration', 'use-dify/nodes/iteration'], + ['guides/workflow/node/loop', 'use-dify/nodes/loop'], + ['guides/workflow/node/code', 'use-dify/nodes/code'], + ['guides/workflow/node/template', 'use-dify/nodes/template'], + ['guides/workflow/node/variable-aggregator', 'use-dify/nodes/variable-aggregator'], + ['guides/workflow/node/doc-extractor', 'use-dify/nodes/doc-extractor'], + ['guides/workflow/node/variable-assigner', 'use-dify/nodes/variable-assigner'], + ['guides/workflow/node/parameter-extractor', 'use-dify/nodes/parameter-extractor'], + ['guides/workflow/node/http-request', 'use-dify/nodes/http-request'], + ['guides/workflow/node/list-operator', 'use-dify/nodes/list-operator'], + ['guides/workflow/node/tools', 'use-dify/nodes/tools'], + ['guides/workflow/node/end', 'use-dify/nodes/output'], + ['guides/workflow/node/llm', 'use-dify/nodes/llm'], + ['getting-started/dify-for-education', 'use-dify/workspace/subscription-management#dify-for-education'], + ['/plugins/schema-definition/model', '/versions/legacy/en/plugins/schema-definition/model/model-designing-rules'], + ['/plugins/schema-definition', '/versions/legacy/en/plugins/schema-definition/manifest'], + ['/zh-hans', 'use-dify/getting-started/introduction'], + ['/introduction', 'use-dify/getting-started/introduction'], + ['introduction', 'use-dify/getting-started/introduction'], + ['/getting-started/readme/features-and-specifications', 'use-dify/getting-started/introduction'], + ['/getting-started/readme/model-providers', 'use-dify/getting-started/introduction'], + ['/getting-started/install-self-hosted', 'self-host/quick-start/docker-compose'], + ['/getting-started/install-self-hosted/docker-compose', 'self-host/quick-start/docker-compose'], + ['/getting-started/install-self-hosted/local-source-code', 'self-host/advanced-deployments/local-source-code'], + ['/getting-started/install-self-hosted/bt-panel', 'self-host/platform-guides/bt-panel'], + ['/getting-started/install-self-hosted/start-the-frontend-docker-container', 'self-host/advanced-deployments/start-the-frontend-docker-container'], + ['/getting-started/install-self-hosted/environments', 'self-host/configuration/environments'], + ['/getting-started/install-self-hosted/faqs', 'self-host/quick-start/faqs'], + ['/getting-started/cloud', 'use-dify/getting-started/introduction'], + ['/getting-started/dify-premium', 'self-host/platform-guides/dify-premium'], + ['guides/application-publishing/launch-your-webapp-quickly/readme', 'use-dify/publish/README'], + ['/plugins/quick-start', 'develop-plugin/getting-started/getting-started-dify-plugin'], + ['/plugins/quick-start/install-plugins', 'develop-plugin/getting-started/getting-started-dify-plugin'], + ['/plugins/quick-start/develop-plugins', 'develop-plugin/dev-guides-and-walkthroughs/cheatsheet'], + ['/plugins/quick-start/develop-plugins/initialize-development-tools', 'develop-plugin/getting-started/cli'], + ['/plugins/quick-start/develop-plugins/tool-plugin', 'develop-plugin/features-and-specs/plugin-types/tool'], + ['/plugins/quick-start/develop-plugins/model-plugin', 'develop-plugin/features-and-specs/plugin-types/model-schema'], + ['/plugins/quick-start/develop-plugins/model-plugin/create-model-providers', 'develop-plugin/dev-guides-and-walkthroughs/creating-new-model-provider'], + ['learn-more/extended-reading/dify-docs-mcp', 'use-dify/build/mcp'], + ['getting-started/cloud', 'use-dify/getting-started/introduction'], + ['getting-started/dify-premium', 'use-dify/getting-started/introduction'], + ['guides/model-configuration/load-balancing', 'use-dify/workspace/model-providers#configure-model-load-balancing'], + ['/guides/model-configuration/load-balancing', 'use-dify/workspace/model-providers#configure-model-load-balancing'], +]) diff --git a/web/eslint-rules/index.js b/web/eslint-rules/index.js index 66c2034625..e879f8ddd4 100644 --- a/web/eslint-rules/index.js +++ b/web/eslint-rules/index.js @@ -1,4 +1,5 @@ import noAsAnyInT from './rules/no-as-any-in-t.js' +import noDeprecatedDocLink from './rules/no-deprecated-doc-link.js' import noExtraKeys from './rules/no-extra-keys.js' import noLegacyNamespacePrefix from './rules/no-legacy-namespace-prefix.js' import requireNsOption from './rules/require-ns-option.js' @@ -12,6 +13,7 @@ const plugin = { }, rules: { 'no-as-any-in-t': noAsAnyInT, + 'no-deprecated-doc-link': noDeprecatedDocLink, 'no-extra-keys': noExtraKeys, 'no-legacy-namespace-prefix': noLegacyNamespacePrefix, 'require-ns-option': requireNsOption, diff --git a/web/eslint-rules/rules/no-deprecated-doc-link.js b/web/eslint-rules/rules/no-deprecated-doc-link.js new file mode 100644 index 0000000000..871aad9530 --- /dev/null +++ b/web/eslint-rules/rules/no-deprecated-doc-link.js @@ -0,0 +1,172 @@ +import { docRedirects } from '../doc-redirects.js' + +/** @type {import('eslint').Rule.RuleModule} */ +export default { + meta: { + type: 'suggestion', + docs: { + description: 'Disallow deprecated documentation paths in useDocLink calls and auto-fix to new paths', + }, + fixable: 'code', + schema: [], + messages: { + deprecatedDocLink: + 'Deprecated documentation path detected. The path "{{oldPath}}" has been moved to "{{newPath}}".', + }, + }, + create(context) { + /** + * Check if the path needs redirect + * @param {string} docPath - Path without language prefix + */ + function checkRedirect(docPath) { + // Normalize path (remove leading slash if present) + const normalizedPath = docPath.startsWith('/') ? docPath.slice(1) : docPath + + if (docRedirects.has(normalizedPath)) { + return { + oldPath: normalizedPath, + newPath: docRedirects.get(normalizedPath), + } + } + + // Also check with leading slash + if (docRedirects.has(`/${normalizedPath}`)) { + return { + oldPath: normalizedPath, + newPath: docRedirects.get(`/${normalizedPath}`).replace(/^\//, ''), + } + } + + return null + } + + /** + * Check if node is a useDocLink call + * @param {import('estree').CallExpression} node + */ + function isUseDocLinkCall(node) { + // Check for direct useDocLink() call that returns a function + // The actual doc path is passed to the returned function + // e.g., const getLink = useDocLink(); getLink('path') + // or useDocLink()('path') + + // For the pattern: useDocLink()('path') + if ( + node.callee.type === 'CallExpression' + && node.callee.callee.type === 'Identifier' + && node.callee.callee.name === 'useDocLink' + ) { + return true + } + + return false + } + + /** + * Check if node is a call to a function returned by useDocLink + * This handles: const docLink = useDocLink(); docLink('path') + */ + function isDocLinkFunctionCall(node, scope) { + if (node.callee.type !== 'Identifier') + return false + + const calleeName = node.callee.name + + // Look for variable declaration that assigns useDocLink() + const variable = scope.variables.find(v => v.name === calleeName) + if (!variable || variable.defs.length === 0) + return false + + const def = variable.defs[0] + if (def.type !== 'Variable' || !def.node.init) + return false + + const init = def.node.init + + // Check if initialized with useDocLink() + if ( + init.type === 'CallExpression' + && init.callee.type === 'Identifier' + && init.callee.name === 'useDocLink' + ) { + return true + } + + return false + } + + /** + * Report a deprecated path and provide fix + */ + function reportDeprecatedPath(node, redirect, sourceCode) { + context.report({ + node, + messageId: 'deprecatedDocLink', + data: { + oldPath: redirect.oldPath, + newPath: redirect.newPath, + }, + fix(fixer) { + // Replace the string value, preserving the quote style + const raw = sourceCode.getText(node) + const quote = raw[0] + return fixer.replaceText(node, `${quote}${redirect.newPath}${quote}`) + }, + }) + } + + /** + * Check a string literal node for deprecated path + */ + function checkStringLiteral(node, sourceCode) { + if (node.type !== 'Literal' || typeof node.value !== 'string') + return + + const redirect = checkRedirect(node.value) + if (redirect) + reportDeprecatedPath(node, redirect, sourceCode) + } + + /** + * Check pathMap object for deprecated paths + * pathMap is like { en: 'path1', zh: 'path2' } + */ + function checkPathMapObject(node, sourceCode) { + if (node.type !== 'ObjectExpression') + return + + for (const prop of node.properties) { + if (prop.type !== 'Property') + continue + + // Check the value of each property + checkStringLiteral(prop.value, sourceCode) + } + } + + return { + CallExpression(node) { + const sourceCode = context.sourceCode || context.getSourceCode() + const scope = sourceCode.getScope?.(node) || context.getScope() + + // Check if this is useDocLink()('path') pattern + const isDirectCall = isUseDocLinkCall(node) + + // Check if this is docLink('path') where docLink = useDocLink() + const isIndirectCall = isDocLinkFunctionCall(node, scope) + + if (!isDirectCall && !isIndirectCall) + return + + // Check first argument (the doc path) + if (node.arguments.length >= 1) + checkStringLiteral(node.arguments[0], sourceCode) + + // Check second argument (pathMap object) + if (node.arguments.length >= 2) + checkPathMapObject(node.arguments[1], sourceCode) + }, + } + }, +} diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index b8191a5eea..654465f437 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -178,6 +178,7 @@ export default antfu( rules: { // 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }], 'dify-i18n/no-as-any-in-t': 'error', + 'dify-i18n/no-deprecated-doc-link': 'error', // 'dify-i18n/no-legacy-namespace-prefix': 'error', // 'dify-i18n/require-ns-option': 'error', }, diff --git a/web/i18n-config/language.ts b/web/i18n-config/language.ts index 28afd9eabf..64c22afe44 100644 --- a/web/i18n-config/language.ts +++ b/web/i18n-config/language.ts @@ -1,3 +1,4 @@ +import type { DocLanguage } from '@/types/doc-paths' import data from './languages' export type Item = { @@ -22,9 +23,9 @@ export const getLanguage = (locale: Locale): Locale => { return LanguagesSupported[0].replace('-', '_') as Locale } -const DOC_LANGUAGE: Record = { - 'zh-Hans': 'zh-hans', - 'ja-JP': 'ja-jp', +const DOC_LANGUAGE: Record = { + 'zh-Hans': 'zh', + 'ja-JP': 'ja', 'en-US': 'en', } @@ -56,7 +57,7 @@ export const localeMap: Record = { 'ar-TN': 'ar', } -export const getDocLanguage = (locale: string) => { +export const getDocLanguage = (locale: string): DocLanguage => { return DOC_LANGUAGE[locale] || 'en' } diff --git a/web/package.json b/web/package.json index 7537b942fb..5ef5a50a6c 100644 --- a/web/package.json +++ b/web/package.json @@ -38,6 +38,7 @@ "type-check:tsgo": "tsgo --noEmit", "prepare": "cd ../ && node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky ./web/.husky", "gen-icons": "node ./scripts/gen-icons.mjs && eslint --fix app/components/base/icons/src/", + "gen-doc-paths": "tsx ./scripts/gen-doc-paths.ts", "uglify-embed": "node ./bin/uglify-embed", "i18n:check": "tsx ./scripts/check-i18n.js", "test": "vitest run", diff --git a/web/scripts/gen-doc-paths.ts b/web/scripts/gen-doc-paths.ts new file mode 100644 index 0000000000..a7b2fef081 --- /dev/null +++ b/web/scripts/gen-doc-paths.ts @@ -0,0 +1,271 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY +// +// This script fetches the docs.json from dify-docs repository +// and generates TypeScript types for documentation paths. +// +// Usage: pnpm gen-doc-paths + +import { writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const DOCS_JSON_URL = 'https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json' +const OUTPUT_PATH = path.resolve(__dirname, '../types/doc-paths.ts') +const REDIRECTS_PATH = path.resolve(__dirname, '../eslint-rules/doc-redirects.js') + +type NavItem = string | NavObject | NavItem[] + +type NavObject = { + pages?: NavItem[] + groups?: NavItem[] + dropdowns?: NavItem[] + languages?: NavItem[] + versions?: NavItem[] + [key: string]: unknown +} + +type Redirect = { + source: string + destination: string +} + +type DocsJson = { + navigation?: NavItem + redirects?: Redirect[] + [key: string]: unknown +} + +/** + * Recursively extract all page paths from navigation structure + */ +function extractPaths(item: NavItem | undefined, paths: Set = new Set()): Set { + if (!item) + return paths + + if (Array.isArray(item)) { + for (const el of item) + extractPaths(el, paths) + + return paths + } + + if (typeof item === 'string') { + paths.add(item) + return paths + } + + if (typeof item === 'object') { + // Handle pages array + if (item.pages) + extractPaths(item.pages, paths) + + // Handle groups array + if (item.groups) + extractPaths(item.groups, paths) + + // Handle dropdowns + if (item.dropdowns) + extractPaths(item.dropdowns, paths) + + // Handle languages + if (item.languages) + extractPaths(item.languages, paths) + + // Handle versions in navigation + if (item.versions) + extractPaths(item.versions, paths) + } + + return paths +} + +/** + * Group paths by their prefix structure + */ +function groupPathsBySection(paths: Set): Record> { + const groups: Record> = {} + + for (const fullPath of paths) { + // Skip non-doc paths (like .json files for OpenAPI) + if (fullPath.endsWith('.json')) + continue + + // Remove language prefix (en/, zh/, ja/) + const withoutLang = fullPath.replace(/^(en|zh|ja)\//, '') + if (!withoutLang || withoutLang === fullPath) + continue + + // Get section (first part of path) + const parts = withoutLang.split('/') + const section = parts[0] + + if (!groups[section]) + groups[section] = new Set() + + groups[section].add(withoutLang) + } + + return groups +} + +/** + * Convert section name to TypeScript type name + */ +function sectionToTypeName(section: string): string { + return section + .split('-') + .map(part => part.charAt(0).toUpperCase() + part.slice(1)) + .join('') +} + +/** + * Generate TypeScript type definitions + */ +function generateTypeDefinitions(groups: Record>): string { + const lines: string[] = [ + '// GENERATE BY script', + '// DON NOT EDIT IT MANUALLY', + '//', + '// Generated from: https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json', + `// Generated at: ${new Date().toISOString()}`, + '', + '// Language prefixes', + 'export type DocLanguage = \'en\' | \'zh\' | \'ja\'', + '', + ] + + const typeNames: string[] = [] + + // Generate type for each section + for (const [section, pathsSet] of Object.entries(groups)) { + const paths = Array.from(pathsSet).sort() + const typeName = `${sectionToTypeName(section)}Path` + typeNames.push(typeName) + + lines.push(`// ${sectionToTypeName(section)} paths`) + lines.push(`export type ${typeName} =`) + + for (const p of paths) { + lines.push(` | '${p}'`) + } + + lines.push('') + } + + // Generate API reference type (for .json files) + lines.push('// API Reference paths') + lines.push('export type ApiReferencePath =') + lines.push(' | \'api-reference/openapi_chat.json\'') + lines.push(' | \'api-reference/openapi_chatflow.json\'') + lines.push(' | \'api-reference/openapi_workflow.json\'') + lines.push(' | \'api-reference/openapi_knowledge.json\'') + lines.push(' | \'api-reference/openapi_completion.json\'') + lines.push('') + typeNames.push('ApiReferencePath') + + // Generate combined type + lines.push('// Combined path without language prefix') + lines.push('export type DocPathWithoutLang =') + for (const typeName of typeNames) { + lines.push(` | ${typeName}`) + } + lines.push('') + + // Generate full path type with language prefix + lines.push('// Full documentation path with language prefix') + // eslint-disable-next-line no-template-curly-in-string + lines.push('export type DifyDocPath = `${DocLanguage}/${DocPathWithoutLang}`') + lines.push('') + + return lines.join('\n') +} + +/** + * Generate redirects map for ESLint rule + * Strips language prefix from paths for use with useDocLink + */ +function generateRedirectsModule(redirects: Redirect[]): string { + const lines: string[] = [ + '// GENERATE BY script', + '// DON NOT EDIT IT MANUALLY', + '//', + '// Generated from: https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json', + `// Generated at: ${new Date().toISOString()}`, + '', + '/** @type {Map} */', + 'export const docRedirects = new Map([', + ] + + // Use a map to deduplicate paths (same path in different languages) + const pathMap = new Map() + const langPrefixRegex = /^\/(en|zh|ja|zh-hans|ja-jp)\// + + for (const redirect of redirects) { + // Skip wildcard redirects + if (redirect.source.includes(':slug')) + continue + + // Strip language prefix from source and destination + const sourceWithoutLang = redirect.source.replace(langPrefixRegex, '') + const destWithoutLang = redirect.destination.replace(langPrefixRegex, '') + + // Only add if we haven't seen this path yet + if (!pathMap.has(sourceWithoutLang)) + pathMap.set(sourceWithoutLang, destWithoutLang) + } + + for (const [source, dest] of pathMap) { + lines.push(` ['${source}', '${dest}'],`) + } + + lines.push('])') + lines.push('') + + return lines.join('\n') +} + +async function main(): Promise { + // eslint-disable-next-line no-console + console.log('Fetching docs.json from GitHub...') + + const response = await fetch(DOCS_JSON_URL) + if (!response.ok) + throw new Error(`Failed to fetch docs.json: ${response.status} ${response.statusText}`) + + const docsJson = await response.json() as DocsJson + // eslint-disable-next-line no-console + console.log('Successfully fetched docs.json') + + // Extract paths from navigation + const allPaths = extractPaths(docsJson.navigation) + + // eslint-disable-next-line no-console + console.log(`Found ${allPaths.size} total paths`) + + // Group by section + const groups = groupPathsBySection(allPaths) + // eslint-disable-next-line no-console + console.log(`Grouped into ${Object.keys(groups).length} sections:`, Object.keys(groups)) + + // Generate TypeScript + const tsContent = generateTypeDefinitions(groups) + + // Write to file + await writeFile(OUTPUT_PATH, tsContent, 'utf-8') + // eslint-disable-next-line no-console + console.log(`Generated TypeScript types at: ${OUTPUT_PATH}`) + + // Generate redirects module for ESLint rule + const redirects = docsJson.redirects || [] + const redirectsContent = generateRedirectsModule(redirects) + await writeFile(REDIRECTS_PATH, redirectsContent, 'utf-8') + // eslint-disable-next-line no-console + console.log(`Generated redirects module at: ${REDIRECTS_PATH} (${redirects.length} redirects)`) +} + +main().catch((err: Error) => { + console.error('Error:', err.message) + process.exit(1) +}) diff --git a/web/types/doc-paths.ts b/web/types/doc-paths.ts new file mode 100644 index 0000000000..011e25e4ee --- /dev/null +++ b/web/types/doc-paths.ts @@ -0,0 +1,176 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY +// +// Generated from: https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json +// Generated at: 2026-01-12T06:51:06.500Z + +// Language prefixes +export type DocLanguage = 'en' | 'zh' | 'ja' + +// UseDify paths +export type UseDifyPath = + | 'use-dify/build/additional-features' + | 'use-dify/build/goto-anything' + | 'use-dify/build/mcp' + | 'use-dify/build/orchestrate-node' + | 'use-dify/build/predefined-error-handling-logic' + | 'use-dify/build/shortcut-key' + | 'use-dify/build/version-control' + | 'use-dify/debug/error-type' + | 'use-dify/debug/history-and-logs' + | 'use-dify/debug/step-run' + | 'use-dify/debug/variable-inspect' + | 'use-dify/getting-started/introduction' + | 'use-dify/getting-started/key-concepts' + | 'use-dify/getting-started/quick-start' + | 'use-dify/knowledge/connect-external-knowledge-base' + | 'use-dify/knowledge/create-knowledge/chunking-and-cleaning-text' + | 'use-dify/knowledge/create-knowledge/import-text-data/readme' + | 'use-dify/knowledge/create-knowledge/import-text-data/sync-from-notion' + | 'use-dify/knowledge/create-knowledge/import-text-data/sync-from-website' + | 'use-dify/knowledge/create-knowledge/introduction' + | 'use-dify/knowledge/create-knowledge/setting-indexing-methods' + | 'use-dify/knowledge/external-knowledge-api' + | 'use-dify/knowledge/integrate-knowledge-within-application' + | 'use-dify/knowledge/knowledge-pipeline/authorize-data-source' + | 'use-dify/knowledge/knowledge-pipeline/create-knowledge-pipeline' + | 'use-dify/knowledge/knowledge-pipeline/knowledge-pipeline-orchestration' + | 'use-dify/knowledge/knowledge-pipeline/manage-knowledge-base' + | 'use-dify/knowledge/knowledge-pipeline/publish-knowledge-pipeline' + | 'use-dify/knowledge/knowledge-pipeline/readme' + | 'use-dify/knowledge/knowledge-pipeline/upload-files' + | 'use-dify/knowledge/knowledge-request-rate-limit' + | 'use-dify/knowledge/manage-knowledge/introduction' + | 'use-dify/knowledge/manage-knowledge/maintain-dataset-via-api' + | 'use-dify/knowledge/manage-knowledge/maintain-knowledge-documents' + | 'use-dify/knowledge/metadata' + | 'use-dify/knowledge/readme' + | 'use-dify/knowledge/test-retrieval' + | 'use-dify/monitor/analysis' + | 'use-dify/monitor/annotation-reply' + | 'use-dify/monitor/integrations/integrate-aliyun' + | 'use-dify/monitor/integrations/integrate-arize' + | 'use-dify/monitor/integrations/integrate-langfuse' + | 'use-dify/monitor/integrations/integrate-langsmith' + | 'use-dify/monitor/integrations/integrate-opik' + | 'use-dify/monitor/integrations/integrate-phoenix' + | 'use-dify/monitor/integrations/integrate-weave' + | 'use-dify/monitor/logs' + | 'use-dify/nodes/agent' + | 'use-dify/nodes/answer' + | 'use-dify/nodes/code' + | 'use-dify/nodes/doc-extractor' + | 'use-dify/nodes/http-request' + | 'use-dify/nodes/ifelse' + | 'use-dify/nodes/iteration' + | 'use-dify/nodes/knowledge-retrieval' + | 'use-dify/nodes/list-operator' + | 'use-dify/nodes/llm' + | 'use-dify/nodes/loop' + | 'use-dify/nodes/output' + | 'use-dify/nodes/parameter-extractor' + | 'use-dify/nodes/question-classifier' + | 'use-dify/nodes/template' + | 'use-dify/nodes/tools' + | 'use-dify/nodes/trigger/overview' + | 'use-dify/nodes/trigger/plugin-trigger' + | 'use-dify/nodes/trigger/schedule-trigger' + | 'use-dify/nodes/trigger/webhook-trigger' + | 'use-dify/nodes/user-input' + | 'use-dify/nodes/variable-aggregator' + | 'use-dify/nodes/variable-assigner' + | 'use-dify/publish/README' + | 'use-dify/publish/developing-with-apis' + | 'use-dify/publish/publish-mcp' + | 'use-dify/publish/webapp/chatflow-webapp' + | 'use-dify/publish/webapp/embedding-in-websites' + | 'use-dify/publish/webapp/web-app-access' + | 'use-dify/publish/webapp/web-app-settings' + | 'use-dify/publish/webapp/workflow-webapp' + | 'use-dify/tutorials/article-reader' + | 'use-dify/tutorials/build-ai-image-generation-app' + | 'use-dify/tutorials/customer-service-bot' + | 'use-dify/tutorials/simple-chatbot' + | 'use-dify/tutorials/twitter-chatflow' + | 'use-dify/workspace/app-management' + | 'use-dify/workspace/model-providers' + | 'use-dify/workspace/personal-account-management' + | 'use-dify/workspace/plugins' + | 'use-dify/workspace/readme' + | 'use-dify/workspace/subscription-management' + | 'use-dify/workspace/team-members-management' + +// SelfHost paths +export type SelfHostPath = + | 'self-host/advanced-deployments/local-source-code' + | 'self-host/advanced-deployments/start-the-frontend-docker-container' + | 'self-host/configuration/environments' + | 'self-host/platform-guides/bt-panel' + | 'self-host/platform-guides/dify-premium' + | 'self-host/quick-start/docker-compose' + | 'self-host/quick-start/faqs' + | 'self-host/troubleshooting/common-issues' + | 'self-host/troubleshooting/docker-issues' + | 'self-host/troubleshooting/integrations' + | 'self-host/troubleshooting/storage-and-migration' + | 'self-host/troubleshooting/weaviate-v4-migration' + +// DevelopPlugin paths +export type DevelopPluginPath = + | 'develop-plugin/dev-guides-and-walkthroughs/agent-strategy-plugin' + | 'develop-plugin/dev-guides-and-walkthroughs/cheatsheet' + | 'develop-plugin/dev-guides-and-walkthroughs/creating-new-model-provider' + | 'develop-plugin/dev-guides-and-walkthroughs/datasource-plugin' + | 'develop-plugin/dev-guides-and-walkthroughs/develop-a-slack-bot-plugin' + | 'develop-plugin/dev-guides-and-walkthroughs/develop-flomo-plugin' + | 'develop-plugin/dev-guides-and-walkthroughs/develop-md-exporter' + | 'develop-plugin/dev-guides-and-walkthroughs/develop-multimodal-data-processing-tool' + | 'develop-plugin/dev-guides-and-walkthroughs/endpoint' + | 'develop-plugin/dev-guides-and-walkthroughs/tool-oauth' + | 'develop-plugin/dev-guides-and-walkthroughs/tool-plugin' + | 'develop-plugin/dev-guides-and-walkthroughs/trigger-plugin' + | 'develop-plugin/features-and-specs/advanced-development/bundle' + | 'develop-plugin/features-and-specs/advanced-development/customizable-model' + | 'develop-plugin/features-and-specs/advanced-development/reverse-invocation' + | 'develop-plugin/features-and-specs/advanced-development/reverse-invocation-app' + | 'develop-plugin/features-and-specs/advanced-development/reverse-invocation-model' + | 'develop-plugin/features-and-specs/advanced-development/reverse-invocation-node' + | 'develop-plugin/features-and-specs/advanced-development/reverse-invocation-tool' + | 'develop-plugin/features-and-specs/plugin-types/general-specifications' + | 'develop-plugin/features-and-specs/plugin-types/model-designing-rules' + | 'develop-plugin/features-and-specs/plugin-types/model-schema' + | 'develop-plugin/features-and-specs/plugin-types/multilingual-readme' + | 'develop-plugin/features-and-specs/plugin-types/persistent-storage-kv' + | 'develop-plugin/features-and-specs/plugin-types/plugin-info-by-manifest' + | 'develop-plugin/features-and-specs/plugin-types/plugin-logging' + | 'develop-plugin/features-and-specs/plugin-types/remote-debug-a-plugin' + | 'develop-plugin/features-and-specs/plugin-types/tool' + | 'develop-plugin/getting-started/cli' + | 'develop-plugin/getting-started/getting-started-dify-plugin' + | 'develop-plugin/publishing/faq/faq' + | 'develop-plugin/publishing/marketplace-listing/plugin-auto-publish-pr' + | 'develop-plugin/publishing/marketplace-listing/release-by-file' + | 'develop-plugin/publishing/marketplace-listing/release-overview' + | 'develop-plugin/publishing/marketplace-listing/release-to-dify-marketplace' + | 'develop-plugin/publishing/marketplace-listing/release-to-individual-github-repo' + | 'develop-plugin/publishing/standards/contributor-covenant-code-of-conduct' + | 'develop-plugin/publishing/standards/privacy-protection-guidelines' + | 'develop-plugin/publishing/standards/third-party-signature-verification' + +// API Reference paths +export type ApiReferencePath = + | 'api-reference/openapi_chat.json' + | 'api-reference/openapi_chatflow.json' + | 'api-reference/openapi_workflow.json' + | 'api-reference/openapi_knowledge.json' + | 'api-reference/openapi_completion.json' + +// Combined path without language prefix +export type DocPathWithoutLang = + | UseDifyPath + | SelfHostPath + | DevelopPluginPath + | ApiReferencePath + +// Full documentation path with language prefix +export type DifyDocPath = `${DocLanguage}/${DocPathWithoutLang}`