Skip to content

Commit edb3656

Browse files
authored
feat: publish pending docs and runtime updates
Publish pending docs/runtime updates from a clean branch based on latest main. Includes Cloud09_Space showcase entry, docs chat docs/header wiring, Notion API follow-ups, and Cloudflare image proxy config.
1 parent ab0e607 commit edb3656

16 files changed

Lines changed: 427 additions & 127 deletions

File tree

.vitepress/theme/Layout.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import DefaultTheme from 'vitepress/theme'
3+
import DocsAssistant from './components/DocsAssistant.vue'
34
import GiscusComment from './components/GiscusComment.vue'
45
import HomeMotion from './components/HomeMotion.vue'
56
@@ -11,6 +12,7 @@ const { Layout } = DefaultTheme
1112
<template #layout-bottom>
1213
<ClientOnly>
1314
<HomeMotion />
15+
<DocsAssistant />
1416
</ClientOnly>
1517
</template>
1618
<template #doc-footer-before>
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
<script setup lang="ts">
2+
import { computed, nextTick, ref } from 'vue'
3+
4+
type ChatMessage = {
5+
id: string
6+
role: 'user' | 'assistant'
7+
parts: { type: 'text'; text: string }[]
8+
}
9+
10+
const api = import.meta.env.VITE_DOCS_CHAT_API || ''
11+
const title = 'NotionNext AI 助手 v2026.07.29'
12+
const welcome =
13+
'你好,我是 NotionNext 文档助手。你可以问我部署、主题、Notion 配置、评论插件和常见排错问题。'
14+
15+
const open = ref(false)
16+
const input = ref('')
17+
const loading = ref(false)
18+
const messagesEl = ref<HTMLElement | null>(null)
19+
const messages = ref<ChatMessage[]>([makeMessage('assistant', welcome)])
20+
21+
const canSend = computed(() => input.value.trim().length > 0 && !loading.value)
22+
23+
function makeMessage(role: ChatMessage['role'], text: string): ChatMessage {
24+
return {
25+
id: `${role}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
26+
role,
27+
parts: [{ type: 'text', text }]
28+
}
29+
}
30+
31+
function textOf(message: ChatMessage) {
32+
return message.parts.map(part => part.text).join('')
33+
}
34+
35+
async function scrollToBottom() {
36+
await nextTick()
37+
if (messagesEl.value) {
38+
messagesEl.value.scrollTop = messagesEl.value.scrollHeight
39+
}
40+
}
41+
42+
async function ask() {
43+
const text = input.value.trim()
44+
if (!text || loading.value) return
45+
46+
const nextMessages = [...messages.value, makeMessage('user', text)]
47+
messages.value = nextMessages
48+
input.value = ''
49+
loading.value = true
50+
await scrollToBottom()
51+
52+
try {
53+
const separator = api.includes('?') ? '&' : '?'
54+
const response = await fetch(`${api}${separator}stream=false`, {
55+
method: 'POST',
56+
headers: { 'content-type': 'application/json' },
57+
body: JSON.stringify({ messages: nextMessages.slice(-6) })
58+
})
59+
const data = await response.json().catch(() => ({}))
60+
const reply = response.ok ? data.text : data.error
61+
62+
messages.value = [
63+
...nextMessages,
64+
makeMessage('assistant', reply || '请求失败,请稍后再试。')
65+
]
66+
} catch {
67+
messages.value = [...nextMessages, makeMessage('assistant', '网络请求失败,请稍后再试。')]
68+
} finally {
69+
loading.value = false
70+
await scrollToBottom()
71+
}
72+
}
73+
</script>
74+
75+
<template>
76+
<div v-if="api" class="docs-assistant">
77+
<section v-if="open" class="docs-assistant-panel" :aria-label="title">
78+
<header class="docs-assistant-header">
79+
<strong>{{ title }}</strong>
80+
<button type="button" aria-label="关闭 AI 助手" @click="open = false">×</button>
81+
</header>
82+
83+
<div ref="messagesEl" class="docs-assistant-messages">
84+
<p
85+
v-for="message in messages"
86+
:key="message.id"
87+
class="docs-assistant-message"
88+
:class="message.role"
89+
>
90+
{{ textOf(message) }}
91+
</p>
92+
<p v-if="loading" class="docs-assistant-message assistant">正在思考...</p>
93+
</div>
94+
95+
<form class="docs-assistant-form" @submit.prevent="ask">
96+
<textarea
97+
v-model="input"
98+
maxlength="1000"
99+
rows="2"
100+
placeholder="输入你的问题"
101+
@keydown.enter.exact.prevent="ask"
102+
/>
103+
<button type="submit" :disabled="!canSend" aria-label="发送">↑</button>
104+
</form>
105+
</section>
106+
107+
<button class="docs-assistant-fab" type="button" @click="open = true">AI 助手</button>
108+
</div>
109+
</template>
110+
111+
<style scoped>
112+
.docs-assistant {
113+
position: fixed;
114+
right: 22px;
115+
bottom: 22px;
116+
z-index: 50;
117+
font-size: 14px;
118+
}
119+
120+
.docs-assistant-fab {
121+
border: 1px solid color-mix(in srgb, var(--vp-c-brand-1) 34%, transparent);
122+
border-radius: 999px;
123+
padding: 11px 18px;
124+
color: #fff;
125+
background: linear-gradient(135deg, #0f766e, #2563eb);
126+
box-shadow: 0 16px 44px color-mix(in srgb, #0f766e 34%, transparent);
127+
font-weight: 700;
128+
}
129+
130+
.docs-assistant-panel {
131+
display: flex;
132+
flex-direction: column;
133+
width: min(420px, calc(100vw - 28px));
134+
height: min(560px, calc(100vh - 92px));
135+
margin-bottom: 12px;
136+
overflow: hidden;
137+
border: 1px solid color-mix(in srgb, var(--vp-c-divider) 70%, transparent);
138+
border-radius: 16px;
139+
background: color-mix(in srgb, var(--vp-c-bg) 96%, transparent);
140+
box-shadow: 0 24px 70px color-mix(in srgb, var(--vp-c-text-1) 18%, transparent);
141+
backdrop-filter: blur(16px);
142+
}
143+
144+
.docs-assistant-header {
145+
display: flex;
146+
align-items: center;
147+
justify-content: space-between;
148+
padding: 14px 16px;
149+
border-bottom: 1px solid color-mix(in srgb, var(--vp-c-divider) 70%, transparent);
150+
color: var(--vp-c-text-1);
151+
}
152+
153+
.docs-assistant-header button {
154+
display: grid;
155+
width: 32px;
156+
height: 32px;
157+
place-items: center;
158+
border: 0;
159+
border-radius: 999px;
160+
color: var(--vp-c-text-2);
161+
background: var(--vp-c-bg-soft);
162+
font-size: 22px;
163+
line-height: 1;
164+
}
165+
166+
.docs-assistant-messages {
167+
display: flex;
168+
flex: 1;
169+
flex-direction: column;
170+
gap: 10px;
171+
min-height: 0;
172+
padding: 14px;
173+
overflow-y: auto;
174+
background: linear-gradient(180deg, var(--vp-c-bg), var(--vp-c-bg-soft));
175+
}
176+
177+
.docs-assistant-message {
178+
max-width: 88%;
179+
margin: 0;
180+
padding: 10px 12px;
181+
border-radius: 14px;
182+
white-space: pre-wrap;
183+
overflow-wrap: anywhere;
184+
line-height: 1.7;
185+
}
186+
187+
.docs-assistant-message.user {
188+
align-self: flex-end;
189+
border-bottom-right-radius: 4px;
190+
color: #fff;
191+
background: linear-gradient(135deg, #2563eb, #0f766e);
192+
}
193+
194+
.docs-assistant-message.assistant {
195+
align-self: flex-start;
196+
border: 1px solid color-mix(in srgb, var(--vp-c-divider) 70%, transparent);
197+
border-bottom-left-radius: 4px;
198+
color: var(--vp-c-text-1);
199+
background: var(--vp-c-bg);
200+
}
201+
202+
.docs-assistant-form {
203+
position: relative;
204+
padding: 12px;
205+
border-top: 1px solid color-mix(in srgb, var(--vp-c-divider) 70%, transparent);
206+
background: var(--vp-c-bg);
207+
}
208+
209+
.docs-assistant-form textarea {
210+
width: 100%;
211+
min-height: 58px;
212+
resize: none;
213+
border: 1px solid color-mix(in srgb, var(--vp-c-divider) 80%, transparent);
214+
border-radius: 12px;
215+
padding: 10px 48px 10px 12px;
216+
outline: none;
217+
color: var(--vp-c-text-1);
218+
background: var(--vp-c-bg-soft);
219+
font: inherit;
220+
}
221+
222+
.docs-assistant-form textarea:focus {
223+
border-color: #0f766e;
224+
background: var(--vp-c-bg);
225+
}
226+
227+
.docs-assistant-form button {
228+
position: absolute;
229+
right: 22px;
230+
bottom: 22px;
231+
width: 34px;
232+
height: 34px;
233+
border: 0;
234+
border-radius: 999px;
235+
color: #fff;
236+
background: #0f766e;
237+
font-size: 20px;
238+
font-weight: 800;
239+
}
240+
241+
button {
242+
cursor: pointer;
243+
}
244+
245+
button:disabled {
246+
cursor: not-allowed;
247+
opacity: 0.45;
248+
}
249+
250+
@media (max-width: 640px) {
251+
.docs-assistant {
252+
right: 14px;
253+
bottom: 14px;
254+
}
255+
}
256+
</style>

.vitepress/theme/index.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,22 @@ import DefaultTheme from 'vitepress/theme'
22
import { useData, useRoute } from 'vitepress'
33
import { onMounted, watch } from 'vue'
44
import type { DefaultTheme as DefaultThemeConfig, EnhanceAppContext } from 'vitepress'
5-
import chat from 'vitepress-chat'
65
import Layout from './Layout.vue'
76
import { cjkTokenize } from '../search-tokenize'
87
import { syncUnreadUpdates, type RecentUpdatedDoc } from './unread-updates'
9-
import 'vitepress-chat/style.css'
108
import './style.css'
119

12-
/** 勿把 tokenize 放进 themeConfig(会序列化进 HTML 导致 JSON 解析失败、全站白屏) */
10+
/** Keep tokenize out of themeConfig serialization; putting functions there breaks page JSON. */
1311
function patchSearchTokenize(siteData: EnhanceAppContext['siteData']) {
1412
const mini = siteData.themeConfig?.search?.options?.miniSearch
1513
if (mini?.options) {
1614
mini.options.tokenize = cjkTokenize
1715
}
1816
}
1917

20-
const chatApi = import.meta.env.VITE_DOCS_CHAT_API
21-
const chatVersion = 'v2026.07.11.1'
22-
const chatLayout = chatApi
23-
? chat(Layout, {
24-
api: chatApi,
25-
buttonText: 'AI 助手',
26-
headerText: `NotionNext AI 助手 ${chatVersion}`,
27-
headerUrl: null,
28-
initialMessage:
29-
'你好,我是 NotionNext 文档助手。你可以直接问我部署、主题、Notion 配置、评论插件等问题。',
30-
filePath: 'ai-assistant-instructions.txt'
31-
})
32-
: { Layout }
33-
3418
export default {
3519
extends: DefaultTheme,
36-
...chatLayout,
20+
Layout,
3721
setup() {
3822
const route = useRoute()
3923
const { theme } = useData()
@@ -50,7 +34,7 @@ export default {
5034

5135
watch(
5236
() => route.path,
53-
(path) => {
37+
path => {
5438
void syncUnreadUpdates(getUpdatedDocs(), getRecentDocs(), path)
5539
}
5640
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
describe('getNotionAPI', () => {
2+
const originalEnv = process.env
3+
4+
beforeEach(() => {
5+
jest.resetModules()
6+
process.env = { ...originalEnv }
7+
delete process.env.API_BASE_URL
8+
})
9+
10+
afterEach(() => {
11+
process.env = originalEnv
12+
})
13+
14+
it('sets the current Notion API host and User-Agent for notion-client', async () => {
15+
const NotionAPI = jest.fn().mockImplementation(() => ({
16+
getPage: jest.fn().mockResolvedValue({})
17+
}))
18+
jest.doMock('notion-client', () => ({ NotionAPI }))
19+
20+
const notionAPI = require('@/lib/db/notion/getNotionAPI').default
21+
await notionAPI.getPage('page-id')
22+
23+
expect(NotionAPI).toHaveBeenCalledWith(
24+
expect.objectContaining({
25+
apiBaseUrl: 'https://app.notion.com/api/v3',
26+
ofetchOptions: {
27+
headers: {
28+
'User-Agent': 'NotionNext (+https://github.com/NotionNext/NotionNext)'
29+
}
30+
}
31+
})
32+
)
33+
})
34+
})

blog.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// 注: process.env.XX是Vercel的环境变量,配置方式见:https://docs.tangly1024.com/article/how-to-config-notion-next#c4768010ae7d44609b744e79e2f9959a
22

33
const BLOG = {
4-
API_BASE_URL: process.env.API_BASE_URL || 'https://www.notion.so/api/v3', // API默认请求地址,可以配置成自己的地址例如:https://[xxxxx].notion.site/api/v3
4+
API_BASE_URL: process.env.API_BASE_URL || 'https://app.notion.com/api/v3', // API默认请求地址,可以配置成自己的地址例如:https://[xxxxx].notion.site/api/v3
55
// Important page_id!!!Duplicate Template from https://tanghh.notion.site/02ab3b8678004aa69e9e415905ef32a5
66
NOTION_PAGE_ID:
77
process.env.NOTION_PAGE_ID ||
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name = "notion-image-proxy"
2+
main = "worker.mjs"
3+
compatibility_date = "2026-07-22"
4+
5+
routes = [
6+
{ pattern = "cdn.tangly1024.com", custom_domain = true }
7+
]

docs/public/_headers

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/assets/chunks/theme.*.js
2+
Cache-Control: public, max-age=0, must-revalidate

docs/public/ai-assistant-instructions.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ Important doc entry points:
1515
- /user-guide/themes/overview
1616
- /user-guide/reference/features
1717
- /user-guide/comments/overview
18+
- /user-guide/plugins/notion-next-docs-chat

docs/user-guide/config/notion-next-api_base_url.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
在blog.config.js 中可以看到多了一行配置:
1717

1818
```Plain Text
19-
API_BASE_URL: process.env.API_BASE_URL || 'https://www.notion.so/api/v3', // API默认请求地址 ,可配置成自己的 https://&lt;xxxx&gt;.notion.site/api/v3
19+
API_BASE_URL: process.env.API_BASE_URL || 'https://app.notion.com/api/v3', // API默认请求地址 ,可配置成自己的 https://&lt;xxxx&gt;.notion.site/api/v3
2020
```
2121

2222
由于Notion官方域名API([https://www.notion.so/api/v3/queryCollection](https://www.notion.so/api/v3/queryCollection)) 无法使用,接口请求均返回530错误,如下图:

0 commit comments

Comments
 (0)