Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
9b4b9bc
refactor: replace copied UI and JSONL transport with shared packages
HsukqiLee Nov 10, 2026
2871b09
chore: consume verified npm UI and transport releases
HsukqiLee Nov 11, 2026
2be3061
fix(fonts): bundle verified originals for clean checkouts and exports
HsukqiLee Nov 11, 2026
ac9d9a6
fix(ci): validate optional theme sources and canonical resource tests
HsukqiLee Nov 11, 2026
37a477f
fix(resources): declare build root and retain test typechecking
HsukqiLee Nov 11, 2026
1318eed
test(cli): enforce on-demand theme installation contract
HsukqiLee Nov 11, 2026
872744c
test(fonts): allow complete CJK export work and resolve native paths
HsukqiLee Nov 11, 2026
c277217
fix(themes): restore original assets required by bundled layouts
HsukqiLee Nov 12, 2026
067b0d0
ci: bound package workers to shared runner capacity
HsukqiLee Nov 12, 2026
9d61ec7
test(libraries): collect source tests independently of build output
HsukqiLee Nov 12, 2026
2a94efc
test(mcp): verify resource assets through a self-contained local pack
HsukqiLee Nov 12, 2026
8593aee
test(gateway): generate upload fixture with the real PPTX writer
HsukqiLee Nov 12, 2026
cb95dfd
fix(themes): use portable paths for registry and extraction imports
HsukqiLee Nov 12, 2026
a6a7e1d
test(e2e): establish workspace and native dialog preconditions
HsukqiLee Nov 12, 2026
360308b
test(e2e): provision a local webfont pack and retain startup diagnostics
HsukqiLee Nov 12, 2026
decbde2
fix(gateway): admit resource font hashes and align browser preconditions
HsukqiLee Nov 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Resource composition hashes commit exact Markdown bytes across platforms.
packages/resources/pages/*.md text eol=lf
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,16 @@ jobs:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @fastppt/fonts verify-fonts
- run: pnpm --filter @fastppt/e2e exec playwright install --with-deps chromium
- run: pnpm typecheck
- run: pnpm lint
- run: pnpm test
- run: pnpm build
# Each package launches its own workers/Slidev builds on the shared runner.
- run: pnpm test --concurrency=2
- run: pnpm build --concurrency=2
- uses: actions/upload-artifact@v4
if: failure()
with:
name: browser-test-results
path: tests/e2e/test-results
if-no-files-found: ignore
12 changes: 10 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ task_plan.md
/ppts/
/decks/
assets/
!packages/components/src/assets/
!packages/components/src/assets/fastppt-icon.svg
!apps/web/src/components/ui/assets/
!apps/web/src/components/ui/assets/fastppt-icon.svg

# FastPPT workspace files
.codex/
Expand Down Expand Up @@ -64,3 +64,11 @@ assets/
/packages/resources/visual-comparison/*-progress.json

!packages/animations/manifest.csv

!packages/fonts/assets/
packages/fonts/assets/*
!packages/fonts/assets/fonts/
packages/fonts/assets/fonts/*
!packages/fonts/assets/fonts/*.ttf
!packages/fonts/assets/licenses/
!packages/fonts/assets/licenses/*.txt
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ FastPPT Web
- 现代浏览器
- Claude Code 或 Codex CLI(只使用对应 Agent 时需要)

源码工作区的共享包从官方 npm registry 消费:`apps/web/package.json` 中的 `@fastrd/ui-foundation`、`@fastrd/ui-vue`,以及 `packages/harness-codex/package.json` 中的 `@fastrd/harness-transport` 均精确锁定 `0.1.2`。在仓库根目录执行 `pnpm install --frozen-lockfile --registry=https://registry.npmjs.org/`,统一使用根 `pnpm-lock.yaml`。更新锁前先用 `npm view <包名>@0.1.2 version dist --json --registry=https://registry.npmjs.org/` 确认版本、tarball 和 provenance;发布尚未可用时等待发布完成,不切换到 GitHub tarball。

### 安装 CLI

全局安装:
Expand Down
10 changes: 6 additions & 4 deletions apps/cli/tests/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ describe('CLI core service integration', () => {
revision: written.revision,
})
const installed = (await run(['skills', 'install'])) as {
statuses: Array<{ skillId: string; state: string }>
statuses: Array<{ skillId: string; kind: string; state: string }>
}
expect(
installed.statuses.filter(
Expand All @@ -271,9 +271,11 @@ describe('CLI core service integration', () => {
item.skillId === 'fastppt-ppt-generate',
),
).toHaveLength(4)
expect(installed.statuses.every((item) => item.state === 'installed')).toBe(
true,
)
for (const item of installed.statuses) {
expect(item, item.skillId).toMatchObject({
state: item.kind === 'theme' ? 'on-demand' : 'installed',
})
}
expect(
await readFile(
join(workspaceRoot, '.agents/skills/fastppt/references/cli.md'),
Expand Down
2 changes: 1 addition & 1 deletion apps/gateway/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,7 @@ export async function createGateway(
// Script-free resource iframes have opaque origins. Their local webfonts
// need CORS, so allow only immutable font reads from that sandbox origin.
const sandboxFont = origin === 'null' && (request.method === 'GET' || request.method === 'HEAD')
&& (/^\/api\/v1\/resources\/assets\/[a-f0-9]{24}\.woff2(?:\?|$)/.test(request.url) || /^\/api\/v1\/animations\/runtime\.js(?:\?|$)/.test(request.url))
&& (/^\/api\/v1\/resources\/assets\/[a-f0-9]+\.woff2(?:\?|$)/.test(request.url) || /^\/api\/v1\/animations\/runtime\.js(?:\?|$)/.test(request.url))
if (sandboxFont) return
if (!origin || config.allowedWebOrigins.includes(origin)) return
request.log.warn({ origin }, 'Rejected browser origin')
Expand Down
21 changes: 11 additions & 10 deletions apps/gateway/tests/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,10 +879,12 @@ describe('gateway', () => {
expect(
foreignLocalOrigin.headers['access-control-allow-origin'],
).toBeUndefined()
const fontPath = '/api/v1/resources/assets/000000000000000000000000.woff2'
expect((await app.inject({ method: 'GET', url: fontPath, headers: { origin: 'null' } })).statusCode).toBe(404)
for (const [method, url] of [['POST', fontPath], ['GET', '/api/v1/workspace'], ['GET', fontPath.replace('.woff2', '.png')]] as const)
expect((await app.inject({ method, url, headers: { origin: 'null' } })).statusCode).toBe(403)
for (const length of [24, 64]) {
const fontPath = `/api/v1/resources/assets/${'0'.repeat(length)}.woff2`
expect((await app.inject({ method: 'GET', url: fontPath, headers: { origin: 'null' } })).statusCode).toBe(404)
for (const [method, url] of [['POST', fontPath], ['GET', '/api/v1/workspace'], ['GET', fontPath.replace('.woff2', '.png')]] as const)
expect((await app.inject({ method, url, headers: { origin: 'null' } })).statusCode).toBe(403)
}
})

it('returns precise not-found errors for runs and browser inspections', async () => {
Expand Down Expand Up @@ -1926,10 +1928,9 @@ describe('gateway', () => {
const fixtureThemes = createThemeFixture()
const app = await makeApp(root, undefined, fixtureThemes)
openApps.push(app)
const sample = resolve(
dirname(fileURLToPath(import.meta.url)),
'../../../references/pptx-renderer/docs/example/1-chart-and-complex/source.pptx',
)
const sample = join(root, 'source.pptx')
const { writeEditablePptx } = await import('@fastppt/slidewave/server')
await writeEditablePptx(exportSnapshot, sample, { embedFonts: false })
const pptx = readFileSync(sample)
const response = await app.inject({
method: 'POST',
Expand All @@ -1940,7 +1941,7 @@ describe('gateway', () => {
themeName: 'import-test',
},
})
expect(response.statusCode).toBe(200)
expect(response.statusCode, response.body).toBe(200)
const result = ImportPptxThemeResultSchema.parse(response.json())
expect(result.themeId).toBe('slidev-theme-import-test')
expect(result.skillId).toBe('fastppt-theme-import-test')
Expand Down Expand Up @@ -2225,7 +2226,7 @@ describe('gateway', () => {
components: expect.any(Array),
message: expect.any(String),
})
})
}, 30_000) // Writes, imports and approves a real PPTX, including registry reload.

it('rejects non-PPTX theme imports before starting extraction', async () => {
const root = mkdtempSync(join(tmpdir(), 'fastppt-import-invalid-'))
Expand Down
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"zod": "catalog:",
"@fastppt/markdown": "workspace:*",
"markdown-it": "^14.3.0",
"@fastppt/components": "workspace:*"
"@fastrd/ui-vue": "0.1.2",
"@fastrd/ui-foundation": "0.1.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/color-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { initColorMode } from '@fastrd/ui-foundation'
import { computed, shallowRef } from 'vue'

const controller = initColorMode({ storageKey: 'fastppt.color-mode' })
const resolved = shallowRef(controller.resolved)
const unsubscribe = controller.subscribe(state => { resolved.value = state.resolved })
export const isDark = computed(() => resolved.value === 'dark')
export const toggleColorMode = () => controller.setMode(isDark.value ? 'light' : 'dark')
if (import.meta.hot) import.meta.hot.dispose(() => { unsubscribe(); controller.dispose() })
2 changes: 1 addition & 1 deletion apps/web/src/components/AppErrorBoundary.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Box, Button, Heading, Panel, Text } from '@fastppt/components'
import { Box, Button, Heading, Panel, Text } from '@fastrd/ui-vue'
import { onErrorCaptured, ref } from 'vue'

const failure = ref(false)
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/components/editor/MarkdownEditor.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
import {
useColorMode,
Box,
Button,
Header,
Panel,
Text,
} from '@fastppt/components'
} from '@fastrd/ui-vue'
import { isDark } from '../../color-mode.js'
import { onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue'
import type { EditorView } from '@codemirror/view'

Expand All @@ -26,7 +26,6 @@ const emit = defineEmits<{
}>()

const editorElement = useTemplateRef<HTMLDivElement>('editorElement')
const { isDark } = useColorMode()
let editorView: EditorView | undefined
let updateTheme: (() => void) | undefined

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/files/FileTree.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Box } from '@fastppt/components'
import { Box } from '@fastrd/ui-vue'
import FileTreeNode from './FileTreeNode.vue'

import type { FileNode } from '@fastppt/protocol'
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/files/FileTreeNode.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Box, Button, Text } from '@fastppt/components'
import { Box, Button, Text } from '@fastrd/ui-vue'
import { computed, shallowRef } from 'vue'

import type { FileNode } from '@fastppt/protocol'
Expand Down
152 changes: 47 additions & 105 deletions apps/web/src/components/themes/ThemeCatalog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Header,
Heading,
Link,
Modal,
Panel,
Svg,
SvgPath,
Expand All @@ -16,8 +17,8 @@ import {
TableHeader,
TableRow,
Text,
} from '@fastppt/components'
import { computed, onUnmounted, shallowRef } from 'vue'
} from '@fastrd/ui-vue'
import { computed, shallowRef } from 'vue'

import { useGatewayClient } from '../../api/useGatewayClient.js'

Expand Down Expand Up @@ -71,12 +72,6 @@ async function openSkill(theme: DeepReadonly<ThemeSummary>): Promise<void> {
}
}

function handleWindowKey(event: KeyboardEvent): void {
if (event.key === 'Escape') closeModal()
}

window.addEventListener('keydown', handleWindowKey)
onUnmounted(() => window.removeEventListener('keydown', handleWindowKey))
</script>

<template>
Expand Down Expand Up @@ -166,85 +161,53 @@ onUnmounted(() => window.removeEventListener('keydown', handleWindowKey))
</Table>
</Box>

<Teleport to="body">
<Box
v-if="detailTheme || skillTheme"
class="modal-backdrop"
role="presentation"
@mousedown.self="closeModal"
>
<Panel
class="theme-modal"
role="dialog"
aria-modal="true"
:aria-labelledby="detailTheme ? 'theme-detail-title' : 'skill-title'"
<Modal
:open="Boolean(detailTheme || skillTheme)"
class="theme-modal"
:labelledby="detailTheme ? 'theme-detail-title' : 'skill-title'"
@close="closeModal"
>
<Header class="modal-header">
<Box v-if="detailTheme">
<Text class="eyebrow">Theme capabilities</Text>
<Heading id="theme-detail-title" as="h2">{{ detailTheme.displayName }}</Heading>
<Text as="p">{{ detailTheme.description }}</Text>
</Box>
<Box v-else-if="skillTheme">
<Text class="eyebrow">Agent skill source</Text>
<Heading id="skill-title" as="h2">{{ skillTheme.skillId }}</Heading>
<Text as="p">{{ skillTheme.skillVersion }} · SKILL.md</Text>
</Box>
<Button
variant="secondary"
class="close-button"
type="button"
aria-label="关闭弹窗"
@click="closeModal"
>
<Header class="modal-header">
<Box v-if="detailTheme">
<Text class="eyebrow">Theme capabilities</Text>
<Heading id="theme-detail-title" as="h2">
{{
detailTheme.displayName
}}
</Heading>
<Text as="p">{{ detailTheme.description }}</Text>
</Box>
<Box v-else-if="skillTheme">
<Text class="eyebrow">Agent skill source</Text>
<Heading id="skill-title" as="h2">
{{
skillTheme.skillId
}}
</Heading>
<Text as="p">{{ skillTheme.skillVersion }} · SKILL.md</Text>
</Box>
<Button
variant="secondary"
class="close-button"
type="button"
aria-label="关闭弹窗"
@click="closeModal"
>
×
</Button>
</Header>
×
</Button>
</Header>

<Box v-if="detailTheme" class="modal-content feature-list">
<Article
v-for="feature in detailTheme.supportedFeatures"
:key="feature.id"
>
<Text as="strong">{{ feature.label }}</Text>
<Text as="code">{{ feature.id }}</Text>
<Text as="p">{{ feature.description }}</Text>
</Article>
<Panel class="layout-list">
<Text as="strong">可用布局</Text>
<Box>
<Text v-for="layout in detailTheme.layouts" :key="layout.id">
{{ layout.label }}
</Text>
</Box>
</Panel>
</Box>
<Box v-else class="modal-content skill-source">
<Text v-if="skillLoading" as="p" class="catalog-state"
>
正在读取 SKILL.md…
</Text
>
<Text v-else-if="skillError" as="p" class="catalog-state error">
{{ skillError }}
</Text>
<Text v-else as="pre"
>
<Text as="code">{{ skillDocument?.content }}</Text>
</Text
>
<Box v-if="detailTheme" class="modal-content feature-list">
<Article v-for="feature in detailTheme.supportedFeatures" :key="feature.id">
<Text as="strong">{{ feature.label }}</Text>
<Text as="code">{{ feature.id }}</Text>
<Text as="p">{{ feature.description }}</Text>
</Article>
<Panel class="layout-list">
<Text as="strong">可用布局</Text>
<Box>
<Text v-for="layout in detailTheme.layouts" :key="layout.id">{{ layout.label }}</Text>
</Box>
</Panel>
</Box>
</Teleport>
<Box v-else class="modal-content skill-source">
<Text v-if="skillLoading" as="p" class="catalog-state">正在读取 SKILL.md…</Text>
<Text v-else-if="skillError" as="p" class="catalog-state error">{{ skillError }}</Text>
<Text v-else as="pre"><Text as="code">{{ skillDocument?.content }}</Text></Text>
</Box>
</Modal>
</Panel>
</template>

Expand Down Expand Up @@ -380,33 +343,12 @@ code {
.catalog-state.error {
color: var(--color-danger);
}
.modal-backdrop {
position: fixed;
z-index: 1000;
inset: 0;
display: grid;
place-items: center;
padding: 24px;
background: var(--color-overlay);
}
.theme-modal {
.theme-modal[open] {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
width: min(820px, 100%);
width: min(820px, calc(100vw - 48px));
max-height: min(760px, calc(100dvh - 48px));
overflow: hidden;
border: 1px solid var(--color-border-strong);
border-radius: 14px;
background: var(--color-panel-raised);
box-shadow: var(--shadow-modal);
}
.modal-header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 24px;
padding: 20px 22px;
border-bottom: 1px solid var(--color-border);
}
.modal-header h2 {
margin-top: 4px;
Expand Down
Loading
Loading