@lobehub/edge-contract 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LobeHub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,20 @@
1
- # Temporary Holding Version
1
+ # @lobehub/edge-contract
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Edge logic that more than one LobeHub repository has to agree on, kept in exactly one place.
4
+ The gateway, landing, lobehub-cloud and lobe-chat import it instead of carrying copies.
5
+
6
+ | Subpath | What it owns |
7
+ | --- | --- |
8
+ | `./landing-locales` | The URL path prefixes landing serves, and the default (`en`, at the bare path). |
9
+ | `./seo-topics` | The discover topics landing server-renders for `?q=`. |
10
+ | `./request-locale` | The app locale list and how a request picks one (`hl`, then the `LOBE_LOCALE` cookie, then Accept-Language). |
11
+ | `./last-workspace-cookie` | The `LOBE_LAST_WORKSPACE` cookie format. |
12
+ | `./acquisition-context` | The signed `lh_acq` first-touch cookie, landing click ids and the attribution params lifted onto `/signin`. |
13
+
14
+ `fixtures/*.json` are golden cases (`{ cases: [{ name, input, expected }] }`) captured from the
15
+ implementations this package replaced. They are frozen: a change that fails one is a behavior
16
+ change every consumer will see, so update the case on purpose, in the same commit.
17
+
18
+ Design: `lobehub-gateway/docs/ryugi/specs/2026-11-06-logic-dedupe-design.md`.
19
+
20
+ Releases: bump `version`, push a `v<version>` tag; CI publishes to npm.
@@ -0,0 +1,12 @@
1
+ export declare const ACQUISITION_CONTEXT_COOKIE = "lh_acq";
2
+ export declare const ACQUISITION_CONTEXT_MAX_AGE_SECONDS: number;
3
+ export interface AcquisitionContext {
4
+ capturedAt: string;
5
+ lhCid: string;
6
+ }
7
+ export declare const parseLandingClickId: (value: unknown) => string | undefined;
8
+ export declare const captureAcquisitionContext: (request: Pick<Request, "url">, now?: Date) => AcquisitionContext | null;
9
+ export declare const signAcquisitionContext: (context: AcquisitionContext, secret: string) => Promise<string>;
10
+ export declare const verifyAcquisitionContext: (token: string, secret: string, now?: Date) => Promise<AcquisitionContext | null>;
11
+ export declare const readAcquisitionContextCookie: (cookieHeader: string | null) => string | null;
12
+ export declare const ATTRIBUTION_PARAMS: ReadonlySet<string>;
@@ -0,0 +1,175 @@
1
+ export const ACQUISITION_CONTEXT_COOKIE = 'lh_acq';
2
+ export const ACQUISITION_CONTEXT_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
3
+ const ACQUISITION_CONTEXT_VERSION = 1;
4
+ const MAX_COOKIE_LENGTH = 1024;
5
+ const MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
6
+ const SIGNING_SCOPE = 'lh-acquisition-context.v1:';
7
+ const textDecoder = new TextDecoder();
8
+ const textEncoder = new TextEncoder();
9
+ const AUTH_PATHS = new Set(['/signin', '/signup']);
10
+ // Landing issues lh_cid with crypto.randomUUID(). Treat it only as an opaque correlation ID.
11
+ const LH_CID_PATTERN = /^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i;
12
+ export const parseLandingClickId = (value) => {
13
+ if (typeof value !== 'string' || !LH_CID_PATTERN.test(value))
14
+ return;
15
+ return value.toLowerCase();
16
+ };
17
+ const sanitizeCapturedAt = (value) => {
18
+ if (typeof value !== 'string')
19
+ return;
20
+ const timestamp = Date.parse(value);
21
+ if (!Number.isFinite(timestamp))
22
+ return;
23
+ return new Date(timestamp).toISOString();
24
+ };
25
+ const sanitizeContext = (value) => {
26
+ if (!value || typeof value !== 'object')
27
+ return null;
28
+ const candidate = value;
29
+ if (candidate.version !== ACQUISITION_CONTEXT_VERSION)
30
+ return null;
31
+ const capturedAt = sanitizeCapturedAt(candidate.capturedAt);
32
+ const lhCid = parseLandingClickId(candidate.lhCid);
33
+ if (!capturedAt || !lhCid)
34
+ return null;
35
+ return { capturedAt, lhCid };
36
+ };
37
+ const serializeContext = (context) => ({
38
+ capturedAt: context.capturedAt,
39
+ lhCid: context.lhCid,
40
+ version: ACQUISITION_CONTEXT_VERSION,
41
+ });
42
+ const encodeBase64Url = (value) => {
43
+ let binary = '';
44
+ for (const byte of value)
45
+ binary += String.fromCharCode(byte);
46
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
47
+ };
48
+ const decodeBase64Url = (value) => {
49
+ if (!/^[\w-]+$/.test(value))
50
+ throw new Error('Invalid base64url value');
51
+ const base64 = value.replaceAll('-', '+').replaceAll('_', '/');
52
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
53
+ const binary = atob(padded);
54
+ const decoded = new Uint8Array(binary.length);
55
+ for (let index = 0; index < binary.length; index += 1) {
56
+ decoded[index] = binary.charCodeAt(index);
57
+ }
58
+ return decoded;
59
+ };
60
+ const importSigningKey = (secret, usages) => crypto.subtle.importKey('raw', textEncoder.encode(secret), { hash: 'SHA-256', name: 'HMAC' }, false, usages);
61
+ const getSigningInput = (payload) => textEncoder.encode(`${SIGNING_SCOPE}${payload}`).buffer;
62
+ const getCaptureUrl = (requestUrl) => {
63
+ if (!AUTH_PATHS.has(requestUrl.pathname))
64
+ return requestUrl;
65
+ const callbackUrl = requestUrl.searchParams.get('callbackUrl');
66
+ if (!callbackUrl)
67
+ return requestUrl;
68
+ try {
69
+ const parsed = new URL(callbackUrl, requestUrl.origin);
70
+ return parsed.origin === requestUrl.origin ? parsed : requestUrl;
71
+ }
72
+ catch {
73
+ return requestUrl;
74
+ }
75
+ };
76
+ export const captureAcquisitionContext = (request, now = new Date()) => {
77
+ const requestUrl = new URL(request.url);
78
+ const captureUrl = getCaptureUrl(requestUrl);
79
+ const lhCid = parseLandingClickId(captureUrl.searchParams.get('lh_cid')) ??
80
+ parseLandingClickId(requestUrl.searchParams.get('lh_cid'));
81
+ if (!lhCid)
82
+ return null;
83
+ return {
84
+ capturedAt: now.toISOString(),
85
+ lhCid,
86
+ };
87
+ };
88
+ export const signAcquisitionContext = async (context, secret) => {
89
+ if (!secret)
90
+ throw new Error('Acquisition context signing secret is required');
91
+ const sanitized = sanitizeContext({
92
+ ...context,
93
+ version: ACQUISITION_CONTEXT_VERSION,
94
+ });
95
+ if (!sanitized)
96
+ throw new Error('Invalid acquisition context');
97
+ const payload = encodeBase64Url(textEncoder.encode(JSON.stringify(serializeContext(sanitized))));
98
+ const key = await importSigningKey(secret, ['sign']);
99
+ const signature = new Uint8Array(await crypto.subtle.sign('HMAC', key, getSigningInput(payload)));
100
+ const token = `${payload}.${encodeBase64Url(signature)}`;
101
+ if (token.length > MAX_COOKIE_LENGTH)
102
+ throw new Error('Acquisition context cookie is too large');
103
+ return token;
104
+ };
105
+ export const verifyAcquisitionContext = async (token, secret, now = new Date()) => {
106
+ if (!token || !secret || token.length > MAX_COOKIE_LENGTH)
107
+ return null;
108
+ try {
109
+ const parts = token.split('.');
110
+ if (parts.length !== 2)
111
+ return null;
112
+ const [payload, encodedSignature] = parts;
113
+ const key = await importSigningKey(secret, ['verify']);
114
+ const valid = await crypto.subtle.verify('HMAC', key, decodeBase64Url(encodedSignature), getSigningInput(payload));
115
+ if (!valid)
116
+ return null;
117
+ const context = sanitizeContext(JSON.parse(textDecoder.decode(decodeBase64Url(payload))));
118
+ if (!context)
119
+ return null;
120
+ const capturedAt = Date.parse(context.capturedAt);
121
+ const nowTimestamp = now.getTime();
122
+ if (capturedAt > nowTimestamp + MAX_FUTURE_SKEW_MS ||
123
+ nowTimestamp - capturedAt > ACQUISITION_CONTEXT_MAX_AGE_SECONDS * 1000) {
124
+ return null;
125
+ }
126
+ return context;
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ };
132
+ export const readAcquisitionContextCookie = (cookieHeader) => {
133
+ if (!cookieHeader)
134
+ return null;
135
+ for (const part of cookieHeader.split(';')) {
136
+ const separator = part.indexOf('=');
137
+ if (separator < 0)
138
+ continue;
139
+ const name = part.slice(0, separator).trim();
140
+ if (name !== ACQUISITION_CONTEXT_COOKIE)
141
+ continue;
142
+ const value = part.slice(separator + 1).trim();
143
+ try {
144
+ return decodeURIComponent(value);
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ return null;
151
+ };
152
+ // Non-utm_* attribution params posthog-js auto-captures (its CAMPAIGN_PARAMS minus the five
153
+ // utm_* keys). Whoever redirects a visitor to /signin lifts these, plus every utm_*, onto the
154
+ // top-level URL so PostHog's $pageview there records them.
155
+ export const ATTRIBUTION_PARAMS = new Set([
156
+ 'gad_source',
157
+ 'mc_cid',
158
+ 'gclid',
159
+ 'gclsrc',
160
+ 'dclid',
161
+ 'gbraid',
162
+ 'wbraid',
163
+ 'fbclid',
164
+ 'msclkid',
165
+ 'twclid',
166
+ 'li_fat_id',
167
+ 'igshid',
168
+ 'ttclid',
169
+ 'rdt_cid',
170
+ 'epik',
171
+ 'qclid',
172
+ 'sccid',
173
+ 'irclid',
174
+ '_kx',
175
+ ]);
@@ -0,0 +1,4 @@
1
+ export declare const landingLocales: readonly ["en", "zh", "zh-TW", "ja", "ko", "de", "es", "ar", "fr", "pt-BR", "ru", "tr", "pl", "nl", "it", "vi-VN", "bg"];
2
+ export type LandingLocale = (typeof landingLocales)[number];
3
+ export declare const DEFAULT_LANDING_LOCALE = "en";
4
+ export declare const isLandingLocale: (value: string | undefined) => value is LandingLocale;
@@ -0,0 +1,22 @@
1
+ // The URL path prefixes landing serves. `en` is the default and lives at the bare path.
2
+ export const landingLocales = [
3
+ 'en',
4
+ 'zh',
5
+ 'zh-TW',
6
+ 'ja',
7
+ 'ko',
8
+ 'de',
9
+ 'es',
10
+ 'ar',
11
+ 'fr',
12
+ 'pt-BR',
13
+ 'ru',
14
+ 'tr',
15
+ 'pl',
16
+ 'nl',
17
+ 'it',
18
+ 'vi-VN',
19
+ 'bg',
20
+ ];
21
+ export const DEFAULT_LANDING_LOCALE = 'en';
22
+ export const isLandingLocale = (value) => Boolean(value && landingLocales.includes(value));
@@ -0,0 +1,7 @@
1
+ export declare const LAST_WORKSPACE_COOKIE = "LOBE_LAST_WORKSPACE";
2
+ export declare const LAST_WORKSPACE_COOKIE_MAX_AGE: number;
3
+ export declare const LAST_WORKSPACE_SLUG_PATTERN: RegExp;
4
+ export declare const LAST_WORKSPACE_SLUG_MIN = 3;
5
+ export declare const LAST_WORKSPACE_SLUG_MAX = 32;
6
+ export declare const formatLastWorkspaceCookie: (userId: string, slug: string) => string | null;
7
+ export declare const parseLastWorkspaceCookie: (raw: string | undefined | null, userId: string | undefined | null) => string | null;
@@ -0,0 +1,30 @@
1
+ // Not httpOnly: the client rewrites it on every explicit workspace switch. It carries no
2
+ // authority (workspace scope is the `X-Workspace-Id` header, checked against membership), so
3
+ // a forged value only sends its own browser to a URL that 404s.
4
+ export const LAST_WORKSPACE_COOKIE = 'LOBE_LAST_WORKSPACE';
5
+ export const LAST_WORKSPACE_COOKIE_MAX_AGE = 60 * 60 * 24 * 90;
6
+ // The same shape as lobehub-cloud's WORKSPACE_SLUG_PATTERN / _MIN / _MAX, kept here rather than
7
+ // imported because that module also carries the reserved-slug tables, which no edge bundle
8
+ // should pay for. Anything failing it is ignored rather than sanitized, so a tampered cookie
9
+ // cannot smuggle `../`, a protocol or CRLF into a Location header.
10
+ export const LAST_WORKSPACE_SLUG_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
11
+ export const LAST_WORKSPACE_SLUG_MIN = 3;
12
+ export const LAST_WORKSPACE_SLUG_MAX = 32;
13
+ const isValidSlug = (slug) => slug.length >= LAST_WORKSPACE_SLUG_MIN &&
14
+ slug.length <= LAST_WORKSPACE_SLUG_MAX &&
15
+ LAST_WORKSPACE_SLUG_PATTERN.test(slug);
16
+ export const formatLastWorkspaceCookie = (userId, slug) => isValidSlug(slug) ? `${userId}:${slug}` : null;
17
+ // Format and ownership only: whether the slug still names a workspace the user belongs to
18
+ // needs the workspace list, so staleness is healed on the client.
19
+ export const parseLastWorkspaceCookie = (raw, userId) => {
20
+ if (!raw || !userId)
21
+ return null;
22
+ // A userId never contains `:`, so the first one is the separator.
23
+ const separator = raw.indexOf(':');
24
+ if (separator <= 0)
25
+ return null;
26
+ if (raw.slice(0, separator) !== userId)
27
+ return null;
28
+ const slug = raw.slice(separator + 1);
29
+ return isValidSlug(slug) ? slug : null;
30
+ };
@@ -0,0 +1,13 @@
1
+ export declare const appLocales: readonly ["ar", "bg-BG", "de-DE", "en-US", "es-ES", "fr-FR", "ja-JP", "ko-KR", "pt-BR", "ru-RU", "tr-TR", "zh-CN", "zh-TW", "vi-VN", "fa-IR", "it-IT", "pl-PL", "nl-NL"];
2
+ export type AppLocale = (typeof appLocales)[number];
3
+ export declare const DEFAULT_APP_LOCALE = "en-US";
4
+ export declare const LOCALE_QUERY_PARAM = "hl";
5
+ export declare const LOCALE_COOKIE = "LOBE_LOCALE";
6
+ export interface LocaleVocabulary<T extends string> {
7
+ fallback: T;
8
+ tags: readonly T[];
9
+ }
10
+ export declare function matchLocaleTag(tag: string | undefined): AppLocale | undefined;
11
+ export declare function matchLocaleTag<T extends string>(tag: string | undefined, tags: readonly T[]): T | undefined;
12
+ export declare function resolveRequestLocale(request: Request): AppLocale;
13
+ export declare function resolveRequestLocale<T extends string>(request: Request, vocabulary: LocaleVocabulary<T>): T;
@@ -0,0 +1,97 @@
1
+ export const appLocales = [
2
+ 'ar',
3
+ 'bg-BG',
4
+ 'de-DE',
5
+ 'en-US',
6
+ 'es-ES',
7
+ 'fr-FR',
8
+ 'ja-JP',
9
+ 'ko-KR',
10
+ 'pt-BR',
11
+ 'ru-RU',
12
+ 'tr-TR',
13
+ 'zh-CN',
14
+ 'zh-TW',
15
+ 'vi-VN',
16
+ 'fa-IR',
17
+ 'it-IT',
18
+ 'pl-PL',
19
+ 'nl-NL',
20
+ ];
21
+ export const DEFAULT_APP_LOCALE = 'en-US';
22
+ export const LOCALE_QUERY_PARAM = 'hl';
23
+ export const LOCALE_COOKIE = 'LOBE_LOCALE';
24
+ const AUTO_LOCALE = 'auto';
25
+ const APP_VOCABULARY = { fallback: DEFAULT_APP_LOCALE, tags: appLocales };
26
+ const decodeCookieValue = (value) => {
27
+ try {
28
+ return decodeURIComponent(value);
29
+ }
30
+ catch {
31
+ return value;
32
+ }
33
+ };
34
+ const readCookie = (header, name) => {
35
+ if (!header)
36
+ return undefined;
37
+ for (const part of header.split(';')) {
38
+ const separator = part.indexOf('=');
39
+ if (separator === -1 || part.slice(0, separator).trim() !== name)
40
+ continue;
41
+ return decodeCookieValue(part.slice(separator + 1).trim());
42
+ }
43
+ return undefined;
44
+ };
45
+ // Accept-Language is case-insensitive but the vocabulary is canonical BCP 47: without this,
46
+ // `zh-cn` misses `zh-CN` and silently degrades to the fallback.
47
+ const canonicalizeTag = (tag) => {
48
+ const [language = '', ...rest] = tag.split('-');
49
+ return [
50
+ language.toLowerCase(),
51
+ ...rest.map((part) => (part.length === 2 ? part.toUpperCase() : part)),
52
+ ].join('-');
53
+ };
54
+ // Aliases resolve to a prefix rather than a fixed tag, so a vocabulary without the aliased
55
+ // language simply finds nothing.
56
+ const aliasPrefix = (lowerTag) => {
57
+ if (lowerTag.startsWith('ar'))
58
+ return 'ar';
59
+ if (lowerTag.startsWith('fa'))
60
+ return 'fa';
61
+ if (lowerTag.startsWith('cn'))
62
+ return 'zh-CN';
63
+ if (lowerTag.startsWith('zh-hans'))
64
+ return 'zh-CN';
65
+ if (lowerTag.startsWith('zh-hant'))
66
+ return 'zh-TW';
67
+ return undefined;
68
+ };
69
+ export function matchLocaleTag(tag, tags = appLocales) {
70
+ if (!tag)
71
+ return undefined;
72
+ const candidate = aliasPrefix(tag.toLowerCase()) ?? canonicalizeTag(tag);
73
+ return tags.find((supported) => supported.startsWith(candidate));
74
+ }
75
+ const parseAcceptLanguage = (header) => (header ?? '')
76
+ .split(',')
77
+ .map((part) => {
78
+ const [tag = '', ...params] = part.trim().split(';');
79
+ const quality = params.map((param) => param.trim()).find((param) => param.startsWith('q='));
80
+ const weight = quality ? Number(quality.slice(2)) : 1;
81
+ return { tag: tag.trim(), weight: Number.isFinite(weight) ? weight : 0 };
82
+ })
83
+ .filter((candidate) => candidate.tag.length > 0 && candidate.tag !== '*')
84
+ .sort((a, b) => b.weight - a.weight)
85
+ .map((candidate) => candidate.tag);
86
+ export function resolveRequestLocale(request, { fallback, tags } = APP_VOCABULARY) {
87
+ const explicit = new URL(request.url).searchParams.get(LOCALE_QUERY_PARAM) ||
88
+ readCookie(request.headers.get('cookie'), LOCALE_COOKIE);
89
+ if (explicit && explicit !== AUTO_LOCALE)
90
+ return matchLocaleTag(explicit, tags) ?? fallback;
91
+ for (const tag of parseAcceptLanguage(request.headers.get('accept-language'))) {
92
+ const matched = matchLocaleTag(tag, tags);
93
+ if (matched)
94
+ return matched;
95
+ }
96
+ return fallback;
97
+ }
@@ -0,0 +1,261 @@
1
+ export declare const SEO_TOPICS: {
2
+ readonly agent: {
3
+ readonly aliyun: "Aliyun";
4
+ readonly baichuan: "Baichuan";
5
+ readonly baidu: "Baidu";
6
+ readonly bilibili: "Bilibili";
7
+ readonly chatgpt: "ChatGPT";
8
+ readonly claude: "Claude";
9
+ readonly 'claude-code': "Claude Code";
10
+ readonly clawdbot: "ClawdBot";
11
+ readonly coding: "Coding";
12
+ readonly codx: "Codx";
13
+ readonly crewai: "CrewAI";
14
+ readonly cursor: "Cursor";
15
+ readonly deepseek: "DeepSeek";
16
+ readonly design: "Design";
17
+ readonly dingtalk: "DingTalk";
18
+ readonly discord: "Discord";
19
+ readonly doubao: "Doubao";
20
+ readonly douyin: "Douyin";
21
+ readonly education: "Education";
22
+ readonly excel: "Microsoft Excel";
23
+ readonly feishu: "Feishu";
24
+ readonly gemini: "Gemini";
25
+ readonly github: "GitHub";
26
+ readonly hunyuan: "Hunyuan";
27
+ readonly image: "Image";
28
+ readonly jd: "JD.com";
29
+ readonly kimi: "Kimi";
30
+ readonly kuaishou: "Kuaishou";
31
+ readonly langchain: "LangChain";
32
+ readonly line: "Line";
33
+ readonly manus: "Manus";
34
+ readonly marketing: "Marketing";
35
+ readonly memory: "Memory";
36
+ readonly moltbot: "MoltBot";
37
+ readonly notion: "Notion";
38
+ readonly office: "Office";
39
+ readonly ollama: "Ollama";
40
+ readonly openai: "OpenAI";
41
+ readonly openclaw: "OpenClaw";
42
+ readonly pdf: "PDF";
43
+ readonly pinduoduo: "Pinduoduo";
44
+ readonly programming: "Programming";
45
+ readonly qwen: "Qwen";
46
+ readonly rag: "RAG";
47
+ readonly reddit: "Reddit";
48
+ readonly research: "Research";
49
+ readonly slack: "Slack";
50
+ readonly taobao: "Taobao";
51
+ readonly telegram: "Telegram";
52
+ readonly tencent: "Tencent";
53
+ readonly tongyi: "Tongyi";
54
+ readonly toutiao: "Toutiao";
55
+ readonly translation: "Translation";
56
+ readonly vscode: "VS Code";
57
+ readonly wechat: "WeChat";
58
+ readonly wecom: "WeCom";
59
+ readonly weibo: "Weibo";
60
+ readonly wenxin: "Wenxin";
61
+ readonly writing: "Writing";
62
+ readonly xiaohongshu: "Xiaohongshu";
63
+ readonly youtube: "YouTube";
64
+ readonly zhipu: "Zhipu";
65
+ readonly 京东: "京东";
66
+ readonly 今日头条: "今日头条";
67
+ readonly 企业微信: "企业微信";
68
+ readonly 公众号: "公众号";
69
+ readonly 写作: "写作";
70
+ readonly 哔哩哔哩: "哔哩哔哩";
71
+ readonly 小红书: "小红书";
72
+ readonly 微信: "微信";
73
+ readonly 微博: "微博";
74
+ readonly 快手: "快手";
75
+ readonly 抖音: "抖音";
76
+ readonly 拼多多: "拼多多";
77
+ readonly 推特: "推特";
78
+ readonly 淘宝: "淘宝";
79
+ readonly 百度: "百度";
80
+ readonly 营销: "营销";
81
+ readonly 论文: "论文";
82
+ readonly 豆包: "豆包";
83
+ readonly 运营: "运营";
84
+ readonly 钉钉: "钉钉";
85
+ readonly 阿里云: "阿里云";
86
+ readonly 飞书: "飞书";
87
+ };
88
+ readonly mcp: {
89
+ readonly aliyun: "Aliyun";
90
+ readonly aws: "AWS";
91
+ readonly azure: "Azure";
92
+ readonly baidu: "Baidu";
93
+ readonly bilibili: "Bilibili";
94
+ readonly bitbucket: "Bitbucket";
95
+ readonly blender: "Blender";
96
+ readonly brave: "Brave";
97
+ readonly chrome: "Chrome";
98
+ readonly confluence: "Confluence";
99
+ readonly deepseek: "DeepSeek";
100
+ readonly dingtalk: "DingTalk";
101
+ readonly discord: "Discord";
102
+ readonly excel: "Microsoft Excel";
103
+ readonly feishu: "Feishu";
104
+ readonly fhir: "FHIR";
105
+ readonly figma: "Figma";
106
+ readonly firebase: "Firebase";
107
+ readonly firecrawl: "Firecrawl";
108
+ readonly gemini: "Gemini";
109
+ readonly git: "Git";
110
+ readonly github: "GitHub";
111
+ readonly gitlab: "GitLab";
112
+ readonly gmail: "Gmail";
113
+ readonly godot: "Godot";
114
+ readonly google: "Google";
115
+ readonly 'google-calendar': "Google Calendar";
116
+ readonly grafana: "Grafana";
117
+ readonly 'home-assistant': "Home Assistant";
118
+ readonly huawei: "Huawei";
119
+ readonly image: "Image";
120
+ readonly jd: "JD.com";
121
+ readonly jetbrains: "JetBrains";
122
+ readonly jira: "Jira";
123
+ readonly kuaishou: "Kuaishou";
124
+ readonly kubernetes: "Kubernetes";
125
+ readonly line: "Line";
126
+ readonly linkedin: "LinkedIn";
127
+ readonly memory: "Memory";
128
+ readonly mongo: "MongoDB";
129
+ readonly mysql: "MySQL";
130
+ readonly neo4j: "Neo4j";
131
+ readonly notion: "Notion";
132
+ readonly obsidian: "Obsidian";
133
+ readonly ollama: "Ollama";
134
+ readonly openapi: "OpenAPI";
135
+ readonly outlook: "Outlook";
136
+ readonly pdf: "PDF";
137
+ readonly perplexity: "Perplexity";
138
+ readonly playwright: "Playwright";
139
+ readonly postgres: "PostgreSQL";
140
+ readonly python: "Python";
141
+ readonly qdrant: "Qdrant";
142
+ readonly rag: "RAG";
143
+ readonly reddit: "Reddit";
144
+ readonly salesforce: "Salesforce";
145
+ readonly sheets: "Google Sheets";
146
+ readonly shopify: "Shopify";
147
+ readonly slack: "Slack";
148
+ readonly spotify: "Spotify";
149
+ readonly sql: "SQL";
150
+ readonly sqlite: "SQLite";
151
+ readonly steam: "Steam";
152
+ readonly stripe: "Stripe";
153
+ readonly supabase: "Supabase";
154
+ readonly taobao: "Taobao";
155
+ readonly tavily: "Tavily";
156
+ readonly telegram: "Telegram";
157
+ readonly tencent: "Tencent";
158
+ readonly unity: "Unity";
159
+ readonly 'unity-editor': "Unity Editor";
160
+ readonly vercel: "Vercel";
161
+ readonly wechat: "WeChat";
162
+ readonly wecom: "WeCom";
163
+ readonly whatsapp: "WhatsApp";
164
+ readonly word: "Microsoft Word";
165
+ readonly 'x-twitter': "X (Twitter)";
166
+ readonly youtube: "YouTube";
167
+ readonly zapier: "Zapier";
168
+ readonly zhihu: "Zhihu";
169
+ readonly 京东: "京东";
170
+ readonly 企业微信: "企业微信";
171
+ readonly 哔哩哔哩: "哔哩哔哩";
172
+ readonly 微信: "微信";
173
+ readonly 快手: "快手";
174
+ readonly 淘宝: "淘宝";
175
+ readonly 百度: "百度";
176
+ readonly 知乎: "知乎";
177
+ readonly 钉钉: "钉钉";
178
+ readonly 阿里云: "阿里云";
179
+ readonly 飞书: "飞书";
180
+ };
181
+ readonly skills: {
182
+ readonly aliyun: "Aliyun";
183
+ readonly aws: "AWS";
184
+ readonly baidu: "Baidu";
185
+ readonly bilibili: "Bilibili";
186
+ readonly chatgpt: "ChatGPT";
187
+ readonly claude: "Claude";
188
+ readonly 'claude-code': "Claude Code";
189
+ readonly clawdbot: "ClawdBot";
190
+ readonly cline: "Cline";
191
+ readonly codex: "Codex";
192
+ readonly codx: "Codx";
193
+ readonly copilot: "GitHub Copilot";
194
+ readonly cursor: "Cursor";
195
+ readonly database: "Database";
196
+ readonly dingtalk: "DingTalk";
197
+ readonly discord: "Discord";
198
+ readonly docker: "Docker";
199
+ readonly excel: "Microsoft Excel";
200
+ readonly feishu: "Feishu";
201
+ readonly figma: "Figma";
202
+ readonly firebase: "Firebase";
203
+ readonly git: "Git";
204
+ readonly github: "GitHub";
205
+ readonly huawei: "Huawei";
206
+ readonly image: "Image";
207
+ readonly jd: "JD.com";
208
+ readonly kuaishou: "Kuaishou";
209
+ readonly kubernetes: "Kubernetes";
210
+ readonly line: "Line";
211
+ readonly manus: "Manus";
212
+ readonly memory: "Memory";
213
+ readonly moltbot: "MoltBot";
214
+ readonly mysql: "MySQL";
215
+ readonly notion: "Notion";
216
+ readonly ollama: "Ollama";
217
+ readonly openclaw: "OpenClaw";
218
+ readonly pdf: "PDF";
219
+ readonly perplexity: "Perplexity";
220
+ readonly pinduoduo: "Pinduoduo";
221
+ readonly playwright: "Playwright";
222
+ readonly postgres: "PostgreSQL";
223
+ readonly python: "Python";
224
+ readonly rag: "RAG";
225
+ readonly reddit: "Reddit";
226
+ readonly redis: "Redis";
227
+ readonly search: "Search";
228
+ readonly slack: "Slack";
229
+ readonly sql: "SQL";
230
+ readonly supabase: "Supabase";
231
+ readonly taobao: "Taobao";
232
+ readonly tavily: "Tavily";
233
+ readonly telegram: "Telegram";
234
+ readonly tencent: "Tencent";
235
+ readonly vscode: "VS Code";
236
+ readonly wechat: "WeChat";
237
+ readonly wecom: "WeCom";
238
+ readonly windsurf: "Windsurf";
239
+ readonly youtube: "YouTube";
240
+ readonly zhihu: "Zhihu";
241
+ readonly 公众号: "公众号";
242
+ readonly 写作: "写作";
243
+ readonly 哔哩哔哩: "哔哩哔哩";
244
+ readonly 小红书: "小红书";
245
+ readonly 微信: "微信";
246
+ readonly 抖音: "抖音";
247
+ readonly 推特: "推特";
248
+ readonly 淘宝: "淘宝";
249
+ readonly 百度: "百度";
250
+ readonly 知乎: "知乎";
251
+ readonly 营销: "营销";
252
+ readonly 论文: "论文";
253
+ readonly 运营: "运营";
254
+ readonly 钉钉: "钉钉";
255
+ readonly 阿里云: "阿里云";
256
+ readonly 飞书: "飞书";
257
+ };
258
+ };
259
+ export type SeoTopicMarket = keyof typeof SEO_TOPICS;
260
+ export declare const hasSeoTopic: (market: SeoTopicMarket, topic: string) => boolean;
261
+ export declare const isClientOnlySearch: (market: SeoTopicMarket, q: unknown) => boolean;