Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
W
WebAgent
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xuchentao
WebAgent
Commits
61c4a3b5
Commit
61c4a3b5
authored
Jul 29, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add versioned site creation templates
parent
b542c952
Changes
20
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
486 additions
and
14 deletions
+486
-14
design.json
astro-template/src/data/design.json
+5
-0
BaseLayout.astro
astro-template/src/layouts/BaseLayout.astro
+2
-1
global.css
astro-template/src/styles/global.css
+13
-0
config.ts
backend/src/config.ts
+1
-0
schemas.ts
backend/src/schemas.ts
+9
-0
server.ts
backend/src/server.ts
+4
-1
create-site.ts
backend/src/sites/create-site.ts
+20
-4
site-template-catalog.test.ts
backend/src/sites/site-template-catalog.test.ts
+39
-0
site-template-catalog.ts
backend/src/sites/site-template-catalog.ts
+101
-0
site-template-architecture.md
docs/site-template-architecture.md
+24
-0
App.tsx
frontend/src/App.tsx
+77
-7
api.ts
frontend/src/api.ts
+2
-1
create-template.css
frontend/src/create-template.css
+54
-0
main.tsx
frontend/src/main.tsx
+1
-0
index.ts
shared/src/index.ts
+52
-0
manifest.json
site-kit/kernel/manifest.json
+6
-0
manifest.json
site-kit/page-templates/brand-story/1.0.0/manifest.json
+23
-0
manifest.json
.../page-templates/professional-services/1.0.0/manifest.json
+23
-0
manifest.json
...t/page-templates/technology-corporate/1.0.0/manifest.json
+23
-0
themes.json
site-kit/themes.json
+7
-0
No files found.
astro-template/src/data/design.json
0 → 100644
View file @
61c4a3b5
{
"pageTemplate"
:
{
"id"
:
"technology-corporate"
,
"version"
:
"1.0.0"
},
"themeId"
:
"violet-tech"
,
"layout"
:
{
"hero"
:
"split"
,
"density"
:
"comfortable"
,
"radius"
:
"soft"
}
}
astro-template/src/layouts/BaseLayout.astro
View file @
61c4a3b5
---
import SeoHead from "../_platform/seo/SeoHead.astro";
import site from "../data/site.json";
import design from "../data/design.json";
interface Props {
title: string;
...
...
@@ -25,5 +26,5 @@ const props = Astro.props;
<SeoHead
{...
props
}
/>
<slot
name=
"head"
/>
</head>
<body><slot
/></body>
<body
data-page-template=
{design.pageTemplate.id}
data-hero=
{design.layout.hero}
data-density=
{design.layout.density}
data-radius=
{design.layout.radius}
><slot
/></body>
</html>
astro-template/src/styles/global.css
View file @
61c4a3b5
...
...
@@ -18,6 +18,19 @@ button, input { font: inherit; }
}
.eyebrow
::before
{
content
:
""
;
width
:
22px
;
height
:
2px
;
background
:
var
(
--gradient-primary
);
}
.section
{
padding
:
112px
0
;
}
body
[
data-density
=
"compact"
]
.section
{
padding
:
78px
0
;
}
body
[
data-radius
=
"square"
]
:where
(
.visual
,
.contact-card
,
.service-grid
article
,
.actions
a
,
.nav-action
)
{
border-radius
:
3px
!important
;
}
body
[
data-hero
=
"centered"
]
.hero-grid
{
grid-template-columns
:
1
fr
;
text-align
:
center
;
}
body
[
data-hero
=
"centered"
]
.hero-copy
{
max-width
:
920px
;
margin
:
0
auto
;
}
body
[
data-hero
=
"centered"
]
.hero-copy
>
p
{
margin-left
:
auto
;
margin-right
:
auto
;
}
body
[
data-hero
=
"centered"
]
.actions
,
body
[
data-hero
=
"centered"
]
.metrics
{
justify-content
:
center
;
}
body
[
data-hero
=
"centered"
]
.visual
{
display
:
none
;
}
body
[
data-page-template
=
"professional-services"
]
.section-heading
{
margin-left
:
auto
;
margin-right
:
auto
;
text-align
:
center
;
}
body
[
data-page-template
=
"professional-services"
]
.section-heading
.eyebrow
{
justify-content
:
center
;
}
body
[
data-page-template
=
"professional-services"
]
.service-grid
article
{
border-top
:
3px
solid
var
(
--color-primary
);
box-shadow
:
none
;
}
body
[
data-page-template
=
"brand-story"
]
h1
,
body
[
data-page-template
=
"brand-story"
]
h2
{
font-family
:
Georgia
,
"Songti SC"
,
serif
;
font-weight
:
500
;
}
body
[
data-page-template
=
"brand-story"
]
.service-grid
{
gap
:
30px
;
}
body
[
data-page-template
=
"brand-story"
]
.service-grid
article
{
border
:
0
;
box-shadow
:
0
22px
60px
color-mix
(
in
srgb
,
var
(
--color-primary
)
10%
,
transparent
);
}
.section-heading
{
max-width
:
680px
;
margin-bottom
:
56px
;
}
.section-heading
h2
{
margin
:
14px
0
18px
;
font-size
:
clamp
(
34px
,
4.6vw
,
58px
);
letter-spacing
:
-.045em
;
line-height
:
1.08
;
}
.section-heading
p
{
margin
:
0
;
color
:
var
(
--color-text-secondary
);
font-size
:
17px
;
line-height
:
1.8
;
}
...
...
backend/src/config.ts
View file @
61c4a3b5
...
...
@@ -46,6 +46,7 @@ export const config = {
rootDir
,
runtimeDir
:
path
.
resolve
(
currentDir
,
"../../.runtime"
),
templateDir
:
path
.
resolve
(
currentDir
,
"../../astro-template"
),
siteKitDir
:
path
.
resolve
(
currentDir
,
"../../site-kit"
),
sitesDir
,
previewsDir
,
productionDir
,
...
...
backend/src/schemas.ts
View file @
61c4a3b5
...
...
@@ -8,6 +8,15 @@ export const createSiteSchema = z.object({
email
:
z
.
string
().
email
(),
phone
:
z
.
string
().
trim
().
max
(
30
).
optional
(),
brandColor
:
z
.
string
().
regex
(
/^#
[
0-9a-fA-F
]{6}
$/
,
"品牌色必须是 6 位十六进制颜色"
),
pageTemplateId
:
z
.
string
().
regex
(
/^
[
a-z0-9-
]
+$/
),
pageTemplateVersion
:
z
.
string
().
regex
(
/^
\d
+
\.\d
+
\.\d
+$/
),
themeId
:
z
.
string
().
regex
(
/^
[
a-z0-9-
]
+$/
),
layout
:
z
.
object
({
hero
:
z
.
enum
([
"split"
,
"centered"
]),
density
:
z
.
enum
([
"comfortable"
,
"compact"
]),
radius
:
z
.
enum
([
"soft"
,
"square"
]),
}),
initialPrompt
:
z
.
string
().
trim
().
max
(
2000
).
optional
().
or
(
z
.
literal
(
""
).
transform
(()
=>
undefined
)),
});
export
const
loginSchema
=
z
.
object
({
...
...
backend/src/server.ts
View file @
61c4a3b5
...
...
@@ -16,6 +16,7 @@ import {
tenantStatusSchema
,
updateDomainSchema
,
versionCommitSchema
,
}
from
"./schemas.js"
;
import
{
CreateSiteService
}
from
"./sites/create-site.js"
;
import
{
SiteTemplateCatalogService
}
from
"./sites/site-template-catalog.js"
;
import
{
SiteRepository
}
from
"./sites/site-repository.js"
;
import
{
SiteVersionService
}
from
"./sites/site-version-service.js"
;
import
{
SiteLifecycleService
}
from
"./sites/site-lifecycle-service.js"
;
...
...
@@ -39,7 +40,8 @@ const sites = new SiteRepository();
const
git
=
new
GitManager
();
const
builds
=
new
BuildManager
();
const
previews
=
new
PreviewProcessManager
();
const
createSite
=
new
CreateSiteService
(
sites
,
git
,
builds
,
previews
);
const
templateCatalog
=
new
SiteTemplateCatalogService
();
const
createSite
=
new
CreateSiteService
(
sites
,
git
,
builds
,
previews
,
templateCatalog
);
const
agentRuns
=
new
AgentRunRepository
();
const
agentProviders
=
createAgentProviders
();
const
versions
=
new
SiteVersionService
(
sites
,
git
,
builds
,
previews
);
...
...
@@ -88,6 +90,7 @@ app.put("/api/agent-settings", async (request) => {
});
app
.
get
(
"/api/sites"
,
async
(
request
)
=>
sites
.
list
(
requireTenant
(
request
.
auth
)));
app
.
get
(
"/api/site-templates"
,
async
()
=>
templateCatalog
.
list
());
app
.
get
(
"/api/archived-sites"
,
async
(
request
)
=>
sites
.
listArchived
(
requireTenant
(
request
.
auth
)));
app
.
get
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId"
,
async
(
request
)
=>
sites
.
get
(
requireTenant
(
request
.
auth
),
request
.
params
.
siteId
));
app
.
post
(
"/api/sites"
,
async
(
request
,
reply
)
=>
reply
.
code
(
201
).
send
(
await
createSite
.
execute
(
requireTenant
(
request
.
auth
),
createSiteSchema
.
parse
(
request
.
body
))));
...
...
backend/src/sites/create-site.ts
View file @
61c4a3b5
...
...
@@ -7,6 +7,7 @@ import { GitManager } from "../git/git-manager.js";
import
{
BuildManager
}
from
"../build/build-manager.js"
;
import
{
PreviewProcessManager
}
from
"../preview/preview-process-manager.js"
;
import
{
SiteRepository
}
from
"./site-repository.js"
;
import
{
SiteTemplateCatalogService
}
from
"./site-template-catalog.js"
;
export
class
CreateSiteService
{
constructor
(
...
...
@@ -14,16 +15,26 @@ export class CreateSiteService {
private
readonly
git
:
GitManager
,
private
readonly
builds
:
BuildManager
,
private
readonly
previews
:
PreviewProcessManager
,
private
readonly
templates
:
SiteTemplateCatalogService
,
)
{}
async
execute
(
tenantId
:
string
,
input
:
CreateSiteInput
):
Promise
<
SiteInfo
>
{
await
this
.
sites
.
ensureRuntime
();
const
selection
=
await
this
.
templates
.
resolve
(
input
.
pageTemplateId
,
input
.
pageTemplateVersion
,
input
.
themeId
,
input
.
layout
);
const
siteId
=
"site_"
+
crypto
.
randomBytes
(
16
).
toString
(
"hex"
);
const
projectPath
=
this
.
sites
.
getProjectPath
(
tenantId
,
siteId
);
const
previewPort
=
await
this
.
sites
.
allocatePreviewPort
();
const
now
=
new
Date
().
toISOString
();
const
site
:
SiteInfo
=
{
tenantId
,
siteId
,
name
:
input
.
name
,
industry
:
input
.
industry
,
status
:
"creating"
,
templateVersion
:
"1.0.0"
,
environmentVersion
:
4
,
tenantId
,
siteId
,
name
:
input
.
name
,
industry
:
input
.
industry
,
status
:
"creating"
,
templateVersion
:
selection
.
kernel
.
version
,
environmentVersion
:
4
,
template
:
{
kernel
:
selection
.
kernel
,
createdFrom
:
{
pageTemplateId
:
selection
.
template
.
id
,
pageTemplateVersion
:
selection
.
template
.
version
,
themeId
:
selection
.
theme
.
id
,
layout
:
input
.
layout
,
},
...(
input
.
initialPrompt
?
{
initialPrompt
:
input
.
initialPrompt
}
:
{}),
},
previewPort
,
previewUrl
:
getPublicPreviewUrl
(
tenantId
,
siteId
),
currentCommit
:
""
,
publishStatus
:
"unpublished"
,
createdAt
:
now
,
updatedAt
:
now
,
};
...
...
@@ -56,12 +67,17 @@ export class CreateSiteService {
siteData
.
organization
.
legalName
=
input
.
name
;
await
writeFile
(
siteDataPath
,
JSON
.
stringify
(
siteData
,
null
,
2
)
+
"
\n
"
,
"utf8"
);
const
homePath
=
path
.
join
(
projectPath
,
"src/data/home.json"
);
const
home
=
JSON
.
parse
(
await
readFile
(
homePath
,
"utf8"
))
as
{
services
:
Array
<
{
number
:
string
;
title
:
string
;
description
:
string
}
>
};
const
home
=
JSON
.
parse
(
await
readFile
(
homePath
,
"utf8"
))
as
Record
<
string
,
unknown
>
&
{
services
:
Array
<
{
number
:
string
;
title
:
string
;
description
:
string
}
>
};
Object
.
assign
(
home
,
selection
.
template
.
content
);
home
.
services
=
input
.
services
.
map
((
service
,
index
)
=>
({
number
:
String
(
index
+
1
).
padStart
(
2
,
"0"
),
title
:
service
,
description
:
"以专业方法和可靠交付,为客户提供高质量的"
+
service
+
"服务。"
}));
await
writeFile
(
homePath
,
JSON
.
stringify
(
home
,
null
,
2
)
+
"
\n
"
,
"utf8"
);
const
themePath
=
path
.
join
(
projectPath
,
"src/styles/theme.css"
);
const
theme
=
(
await
readFile
(
themePath
,
"utf8"
)).
replaceAll
(
"#7028ff"
,
input
.
brandColor
.
toLowerCase
());
await
writeFile
(
themePath
,
theme
,
"utf8"
);
await
writeFile
(
themePath
,
this
.
templates
.
renderTheme
(
selection
.
theme
,
input
.
brandColor
),
"utf8"
);
await
writeFile
(
path
.
join
(
projectPath
,
"src/data/design.json"
),
JSON
.
stringify
({
pageTemplate
:
{
id
:
selection
.
template
.
id
,
version
:
selection
.
template
.
version
},
themeId
:
selection
.
theme
.
id
,
layout
:
input
.
layout
,
},
null
,
2
)
+
"
\n
"
,
"utf8"
);
await
writeFile
(
path
.
join
(
projectPath
,
"webagent.site.json"
),
JSON
.
stringify
(
site
.
template
,
null
,
2
)
+
"
\n
"
,
"utf8"
);
const
dist
=
await
this
.
builds
.
build
(
projectPath
,
"create_"
+
siteId
,
{
basePath
:
getPublicPreviewUrl
(
tenantId
,
siteId
),
indexable
:
false
,
articlesDirectory
,
articleImagesDirectory
,
});
...
...
backend/src/sites/site-template-catalog.test.ts
0 → 100644
View file @
61c4a3b5
import
assert
from
"node:assert/strict"
;
import
test
from
"node:test"
;
import
{
SiteTemplateCatalogService
}
from
"./site-template-catalog.js"
;
test
(
"catalog exposes the stable kernel and published page templates"
,
async
()
=>
{
const
service
=
new
SiteTemplateCatalogService
();
const
catalog
=
await
service
.
list
();
assert
.
deepEqual
(
catalog
.
kernel
,
{
id
:
"official-site"
,
version
:
"1.0.0"
});
assert
.
deepEqual
(
catalog
.
pageTemplates
.
map
((
template
)
=>
template
.
id
).
sort
(),
[
"brand-story"
,
"professional-services"
,
"technology-corporate"
,
]);
assert
.
equal
(
catalog
.
themes
.
length
,
5
);
});
test
(
"catalog resolves only supported template, theme and layout combinations"
,
async
()
=>
{
const
service
=
new
SiteTemplateCatalogService
();
const
selection
=
await
service
.
resolve
(
"technology-corporate"
,
"1.0.0"
,
"ocean-blue"
,
{
hero
:
"split"
,
density
:
"compact"
,
radius
:
"soft"
,
});
assert
.
equal
(
selection
.
kernel
.
version
,
"1.0.0"
);
assert
.
equal
(
selection
.
template
.
id
,
"technology-corporate"
);
assert
.
equal
(
selection
.
theme
.
id
,
"ocean-blue"
);
await
assert
.
rejects
(()
=>
service
.
resolve
(
"technology-corporate"
,
"1.0.0"
,
"warm-amber"
,
{
hero
:
"split"
,
density
:
"compact"
,
radius
:
"soft"
,
}),
/不支持所选配色/
);
});
test
(
"theme renderer emits a complete semantic palette and custom primary color"
,
async
()
=>
{
const
service
=
new
SiteTemplateCatalogService
();
const
{
theme
}
=
await
service
.
resolve
(
"brand-story"
,
"1.0.0"
,
"warm-amber"
,
{
hero
:
"centered"
,
density
:
"comfortable"
,
radius
:
"soft"
,
});
const
css
=
service
.
renderTheme
(
theme
,
"#123456"
);
assert
.
match
(
css
,
/--color-primary: #123456;/
);
assert
.
match
(
css
,
/--color-text-primary:/
);
assert
.
match
(
css
,
/--color-border:/
);
assert
.
match
(
css
,
/--gradient-primary:/
);
assert
.
doesNotMatch
(
css
,
/function shade/
);
});
backend/src/sites/site-template-catalog.ts
0 → 100644
View file @
61c4a3b5
import
path
from
"node:path"
;
import
{
readFile
,
readdir
}
from
"node:fs/promises"
;
import
type
{
PageTemplateInfo
,
SiteLayoutParameters
,
SiteTemplateCatalog
,
ThemePresetInfo
}
from
"@webagent/shared"
;
import
{
config
}
from
"../config.js"
;
type
ThemeDefinition
=
ThemePresetInfo
&
{
primary
:
string
;
hover
:
string
;
light
:
string
;
secondary
:
string
;
dark
:
string
;
background
:
string
;
soft
:
string
;
text
:
string
;
textSecondary
:
string
;
muted
:
string
;
border
:
string
;
};
type
PageTemplateDefinition
=
PageTemplateInfo
&
{
status
:
"draft"
|
"testing"
|
"published"
|
"retired"
;
compatibleKernel
:
string
;
content
:
{
eyebrow
:
string
;
heroTitle
:
string
;
heroDescription
:
string
;
primaryAction
:
string
;
secondaryAction
:
string
;
metrics
:
Array
<
{
value
:
string
;
label
:
string
}
>
;
advantages
:
Array
<
{
title
:
string
;
description
:
string
}
>
;
};
};
export
class
SiteTemplateCatalogService
{
async
list
():
Promise
<
SiteTemplateCatalog
>
{
const
kernel
=
await
this
.
kernel
();
const
[
themes
,
pageTemplates
]
=
await
Promise
.
all
([
this
.
themes
(),
this
.
pageTemplates
()]);
return
{
kernel
,
themes
:
themes
.
map
(({
id
,
name
,
colors
})
=>
({
id
,
name
,
colors
})),
pageTemplates
:
pageTemplates
.
map
(({
status
:
_status
,
compatibleKernel
:
_compatible
,
content
:
_content
,
...
template
})
=>
template
),
};
}
async
resolve
(
pageTemplateId
:
string
,
version
:
string
,
themeId
:
string
,
layout
:
SiteLayoutParameters
)
{
const
[
kernel
,
templates
,
themes
]
=
await
Promise
.
all
([
this
.
kernel
(),
this
.
pageTemplates
(),
this
.
themes
()]);
const
template
=
templates
.
find
((
item
)
=>
item
.
id
===
pageTemplateId
&&
item
.
version
===
version
);
if
(
!
template
)
throw
Object
.
assign
(
new
Error
(
"页面模板不存在或已下架"
),
{
statusCode
:
400
});
if
(
!
template
.
compatibleKernel
.
startsWith
(
kernel
.
version
.
split
(
"."
)[
0
]
+
"."
))
throw
new
Error
(
"页面模板与当前官网内核不兼容"
);
if
(
!
template
.
themeIds
.
includes
(
themeId
))
throw
Object
.
assign
(
new
Error
(
"当前页面模板不支持所选配色"
),
{
statusCode
:
400
});
const
theme
=
themes
.
find
((
item
)
=>
item
.
id
===
themeId
);
if
(
!
theme
)
throw
Object
.
assign
(
new
Error
(
"配色模板不存在"
),
{
statusCode
:
400
});
for
(
const
key
of
[
"hero"
,
"density"
,
"radius"
]
as
const
)
{
if
(
!
(
template
.
layouts
[
key
]
as
string
[]).
includes
(
layout
[
key
]))
throw
Object
.
assign
(
new
Error
(
`页面模板不支持所选
${
key
}
参数`
),
{
statusCode
:
400
});
}
return
{
kernel
,
template
,
theme
};
}
renderTheme
(
theme
:
ThemeDefinition
,
brandColor
:
string
):
string
{
const
primary
=
brandColor
.
toLowerCase
();
const
hover
=
shade
(
primary
,
-
16
);
const
light
=
shade
(
primary
,
24
);
return
`:root {
--color-primary:
${
primary
}
;
--color-primary-hover:
${
hover
}
;
--color-primary-light:
${
light
}
;
--color-secondary:
${
theme
.
secondary
}
;
--color-primary-dark:
${
theme
.
dark
}
;
--color-background:
${
theme
.
background
}
;
--color-background-soft:
${
theme
.
soft
}
;
--color-surface: #ffffff;
--color-text-primary:
${
theme
.
text
}
;
--color-text-secondary:
${
theme
.
textSecondary
}
;
--color-text-muted:
${
theme
.
muted
}
;
--color-border:
${
theme
.
border
}
;
--color-divider:
${
theme
.
border
}
;
--gradient-primary: linear-gradient(135deg,
${
primary
}
0%,
${
theme
.
secondary
}
100%);
--gradient-dark: linear-gradient(135deg,
${
theme
.
dark
}
0%,
${
theme
.
primary
}
60%,
${
theme
.
secondary
}
100%);
--gradient-soft: linear-gradient(180deg,
${
theme
.
soft
}
0%, #ffffff 100%);
}
`
;
}
private
async
kernel
():
Promise
<
{
id
:
string
;
version
:
string
}
>
{
const
value
=
JSON
.
parse
(
await
readFile
(
path
.
join
(
config
.
siteKitDir
,
"kernel"
,
"manifest.json"
),
"utf8"
))
as
{
id
:
string
;
version
:
string
;
status
:
string
};
if
(
value
.
status
!==
"stable"
)
throw
new
Error
(
"当前没有可用的稳定官网内核"
);
return
{
id
:
value
.
id
,
version
:
value
.
version
};
}
private
async
themes
():
Promise
<
ThemeDefinition
[]
>
{
return
JSON
.
parse
(
await
readFile
(
path
.
join
(
config
.
siteKitDir
,
"themes.json"
),
"utf8"
))
as
ThemeDefinition
[];
}
private
async
pageTemplates
():
Promise
<
PageTemplateDefinition
[]
>
{
const
root
=
path
.
join
(
config
.
siteKitDir
,
"page-templates"
);
const
templateDirectories
=
await
readdir
(
root
,
{
withFileTypes
:
true
});
const
manifests
:
PageTemplateDefinition
[]
=
[];
for
(
const
templateDirectory
of
templateDirectories
.
filter
((
entry
)
=>
entry
.
isDirectory
()))
{
const
versionRoot
=
path
.
join
(
root
,
templateDirectory
.
name
);
const
versions
=
await
readdir
(
versionRoot
,
{
withFileTypes
:
true
});
for
(
const
version
of
versions
.
filter
((
entry
)
=>
entry
.
isDirectory
()))
{
const
manifest
=
JSON
.
parse
(
await
readFile
(
path
.
join
(
versionRoot
,
version
.
name
,
"manifest.json"
),
"utf8"
))
as
PageTemplateDefinition
;
if
(
manifest
.
status
===
"published"
)
manifests
.
push
(
manifest
);
}
}
return
manifests
.
sort
((
a
,
b
)
=>
a
.
name
.
localeCompare
(
b
.
name
,
"zh-CN"
));
}
}
function
shade
(
hex
:
string
,
amount
:
number
):
string
{
const
value
=
Number
.
parseInt
(
hex
.
slice
(
1
),
16
);
const
adjust
=
(
channel
:
number
)
=>
Math
.
max
(
0
,
Math
.
min
(
255
,
channel
+
Math
.
round
(
255
*
amount
/
100
)));
const
channels
=
[
value
>>
16
,
(
value
>>
8
)
&
255
,
value
&
255
].
map
(
adjust
);
return
"#"
+
channels
.
map
((
channel
)
=>
channel
.
toString
(
16
).
padStart
(
2
,
"0"
)).
join
(
""
);
}
docs/site-template-architecture.md
0 → 100644
View file @
61c4a3b5
# 官网模板架构
官网创建由三个独立版本域组成:
1.
**内核版本**
:平台维护的构建、SEO/GEO、文章协议与发布能力。普通用户不选择,站点保存精确版本。
2.
**页面成品版本**
:用户在创建时选择的页面形状、默认内容和视觉方向。创建后成为站点自己的页面,不自动跟随模板升级。
3.
**站点 Git 版本**
:用户和 Agent 对生成结果的持续修改历史。
## 目录职责
-
`site-kit/kernel/manifest.json`
:当前稳定内核的身份和版本。
-
`site-kit/page-templates/<id>/<version>/manifest.json`
:已发布页面成品及其兼容范围、默认内容、支持配色和布局参数。
-
`site-kit/themes.json`
:完整语义配色方案。
-
`astro-template/`
:当前内核运行载体和页面渲染契约。
页面成品发布后不可原地修改;任何调整必须创建新版本。创建服务只接受目录中已发布且与当前内核兼容的精确版本组合。
## 创建不变量
-
先生成并构建不依赖模型的基础成品。
-
用户未填写 Prompt 时,基础成品直接成为第一版官网。
-
用户填写 Prompt 时,Agent 只在站点草稿中修改并接受路径与构建校验。
-
智能定制成功后保存为第二个 Git 版本;失败则丢弃草稿,保留已经可用的基础成品。
-
`metadata/site.json`
和站点根目录的
`webagent.site.json`
记录精确创建来源;页面模板后续升级不会覆盖已有网站。
frontend/src/App.tsx
View file @
61c4a3b5
This diff is collapsed.
Click to expand it.
frontend/src/api.ts
View file @
61c4a3b5
import
type
{
AgentRunEvent
,
AgentRunInfo
,
AgentSettings
,
ArticleCompileResult
,
ArticleDocument
,
ArticleImageUploadResult
,
ArticleInput
,
ArticleListResult
,
ArticleMutationResult
,
CreateSiteInput
,
CreateTenantInput
,
DomainBinding
,
DomainListResult
,
DraftPreviewResult
,
GitHistoryItem
,
PreviewVersionResult
,
PublishResult
,
SessionInfo
,
SiteInfo
,
TenantAdminInfo
,
TenantAdminSiteInfo
}
from
"@webagent/shared"
;
import
type
{
AgentRunEvent
,
AgentRunInfo
,
AgentSettings
,
ArticleCompileResult
,
ArticleDocument
,
ArticleImageUploadResult
,
ArticleInput
,
ArticleListResult
,
ArticleMutationResult
,
CreateSiteInput
,
CreateTenantInput
,
DomainBinding
,
DomainListResult
,
DraftPreviewResult
,
GitHistoryItem
,
PreviewVersionResult
,
PublishResult
,
SessionInfo
,
SiteInfo
,
SiteTemplateCatalog
,
TenantAdminInfo
,
TenantAdminSiteInfo
}
from
"@webagent/shared"
;
async
function
request
<
T
>
(
url
:
string
,
options
?:
RequestInit
):
Promise
<
T
>
{
const
headers
=
new
Headers
(
options
?.
headers
);
...
...
@@ -26,6 +26,7 @@ export const api = {
agentSettings
:
()
=>
request
<
AgentSettings
>
(
"/api/agent-settings"
),
updateAgentSettings
:
(
provider
:
AgentSettings
[
"provider"
])
=>
request
<
AgentSettings
>
(
"/api/agent-settings"
,
{
method
:
"PUT"
,
body
:
JSON
.
stringify
({
provider
})
}),
sites
:
()
=>
request
<
SiteInfo
[]
>
(
"/api/sites"
),
siteTemplates
:
()
=>
request
<
SiteTemplateCatalog
>
(
"/api/site-templates"
),
archivedSites
:
()
=>
request
<
SiteInfo
[]
>
(
"/api/archived-sites"
),
site
:
(
siteId
:
string
)
=>
request
<
SiteInfo
>
(
"/api/sites/"
+
siteId
),
createSite
:
(
input
:
CreateSiteInput
)
=>
request
<
SiteInfo
>
(
"/api/sites"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
(
input
)
}),
...
...
frontend/src/create-template.css
0 → 100644
View file @
61c4a3b5
.workspace-create-card
{
width
:
min
(
940px
,
100%
);
}
.template-loading
{
margin
:
auto
;
display
:
flex
;
align-items
:
center
;
gap
:
9px
;
color
:
var
(
--muted
);
font-size
:
12px
;
}
.template-catalog-error
{
margin
:
auto
;
}
.create-section
{
padding
:
25px
0
;
border-top
:
1px
solid
var
(
--divider
);
}
.create-section
>
header
{
display
:
flex
;
align-items
:
center
;
gap
:
9px
;
margin-bottom
:
17px
;
}
.create-section
>
header
span
{
display
:
grid
;
place-items
:
center
;
width
:
24px
;
height
:
24px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
7px
;
font-size
:
9px
;
font-weight
:
800
;
}
.create-section
>
header
h3
{
margin
:
0
;
font-size
:
14px
;
}
.template-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
1
fr
);
gap
:
12px
;
}
.template-card
{
position
:
relative
;
min-width
:
0
;
padding
:
8px
;
text-align
:
left
;
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
13px
;
transition
:
.2s
;
}
.template-card
:hover
{
border-color
:
#bba3f3
;
transform
:
translateY
(
-2px
);
}
.template-card.active
{
border-color
:
var
(
--primary
);
box-shadow
:
0
0
0
2px
rgba
(
112
,
40
,
255
,
.08
);
}
.template-card
>
span
:nth-child
(
2
)
{
display
:
flex
;
flex-direction
:
column
;
padding
:
11px
6px
7px
;
}
.template-card
strong
{
font-size
:
11px
;
}
.template-card
small
{
margin-top
:
5px
;
color
:
var
(
--muted
);
font-size
:
8px
;
line-height
:
1.55
;
}
.template-card
em
{
margin-top
:
8px
;
color
:
var
(
--primary
);
font-size
:
7px
;
font-style
:
normal
;
}
.template-card
>
i
{
position
:
absolute
;
right
:
12px
;
top
:
12px
;
display
:
grid
;
place-items
:
center
;
width
:
20px
;
height
:
20px
;
color
:
#fff
;
background
:
var
(
--primary
);
border
:
2px
solid
#fff
;
border-radius
:
50%
;
}
.template-preview
{
height
:
132px
;
display
:
flex
!important
;
flex-direction
:
column
!important
;
overflow
:
hidden
;
padding
:
0
!important
;
border-radius
:
8px
;
}
.preview-nav
{
height
:
24px
;
padding
:
0
10px
;
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
background
:
rgba
(
255
,
255
,
255
,
.86
);
}
.preview-nav
b
{
width
:
11px
;
height
:
11px
;
border-radius
:
3px
;
}
.preview-nav
small
{
width
:
18px
;
height
:
2px
;
margin
:
0
;
background
:
#c6c2cf
;
border-radius
:
2px
;
}
.preview-nav
small
:first-of-type
{
margin-left
:
auto
;
}
.preview-body
{
flex
:
1
;
display
:
grid
;
grid-template-columns
:
1.15
fr
.85
fr
;
align-items
:
center
;
gap
:
11px
;
padding
:
13px
;
}
.preview-body
>
span
:first-child
{
display
:
flex
;
flex-direction
:
column
;
gap
:
5px
;
}
.preview-body
>
span
small
{
width
:
26px
;
height
:
3px
;
margin
:
0
;
}
.preview-body
strong
{
width
:
84%
;
height
:
6px
;
background
:
#2d2b35
;
border-radius
:
2px
;
}
.preview-body
strong
+
strong
{
width
:
65%
;
}
.preview-body
em
{
width
:
92%
;
height
:
3px
;
background
:
#a6a1ae
;
}
.preview-body
b
{
width
:
34px
;
height
:
10px
;
border-radius
:
3px
;
}
.preview-art
{
height
:
60px
;
border-radius
:
8px
;
}
.preview-cards
{
height
:
26px
;
padding
:
0
13px
9px
;
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
1
fr
);
gap
:
6px
;
}
.preview-cards
small
{
height
:
17px
;
margin
:
0
;
background
:
rgba
(
255
,
255
,
255
,
.92
);
border
:
1px
solid
rgba
(
50
,
45
,
65
,
.08
);
border-radius
:
4px
;
}
.preset-label
{
margin-bottom
:
9px
;
color
:
var
(
--text-2
);
font-size
:
10px
;
font-weight
:
700
;
}
.theme-options
{
display
:
grid
;
grid-template-columns
:
repeat
(
5
,
1
fr
);
gap
:
8px
;
}
.theme-options
button
{
min-width
:
0
;
height
:
52px
;
padding
:
7px
9px
;
display
:
grid
;
grid-template-columns
:
1
fr
auto
;
grid-template-rows
:
auto
auto
;
align-items
:
center
;
text-align
:
left
;
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
9px
;
}
.theme-options
button
.active
{
border-color
:
var
(
--primary
);
box-shadow
:
0
0
0
2px
rgba
(
112
,
40
,
255
,
.07
);
}
.theme-options
button
>
span
{
display
:
flex
;
grid-column
:
1
/
-1
;
}
.theme-options
button
i
{
width
:
17px
;
height
:
8px
;
}
.theme-options
button
i
:first-child
{
border-radius
:
4px
0
0
4px
;
}
.theme-options
button
i
:last-child
{
border-radius
:
0
4px
4px
0
;
}
.theme-options
button
strong
{
font-size
:
8px
;
}
.theme-options
button
svg
{
color
:
var
(
--primary
);
}
.brand-color-control
{
margin-top
:
13px
;
display
:
grid
;
grid-template-columns
:
78px
1
fr
;
align-items
:
center
;
color
:
var
(
--text-2
);
font-size
:
9px
;
font-weight
:
700
;
}
.brand-color-control
label
{
height
:
38px
;
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
padding
:
4px
9px
;
background
:
#fbfbfd
;
border
:
1px
solid
var
(
--border
);
border-radius
:
8px
;
}
.brand-color-control
input
{
width
:
28px
;
height
:
28px
;
padding
:
1px
;
border
:
0
;
background
:
transparent
;
}
.brand-color-control
code
{
color
:
var
(
--text-2
);
font-size
:
9px
;
}
.brand-color-control
small
{
margin-left
:
auto
;
color
:
var
(
--muted
);
font-size
:
8px
;
font-weight
:
500
;
}
.create-parameter-grid
{
margin-top
:
16px
;
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
1
fr
);
gap
:
12px
;
}
.preset-buttons
>
span
{
display
:
block
;
margin-bottom
:
7px
;
color
:
var
(
--text-2
);
font-size
:
9px
;
font-weight
:
700
;
}
.preset-buttons
>
div
{
display
:
flex
;
padding
:
3px
;
background
:
#f5f4f8
;
border-radius
:
8px
;
}
.preset-buttons
button
{
flex
:
1
;
height
:
29px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
6px
;
font-size
:
8px
;
font-weight
:
700
;
}
.preset-buttons
button
.active
{
color
:
var
(
--primary
);
background
:
#fff
;
box-shadow
:
0
2px
7px
rgba
(
32
,
24
,
52
,
.08
);
}
.form-warning
{
display
:
flex
;
align-items
:
flex-start
;
gap
:
8px
;
margin-top
:
16px
;
padding
:
11px
13px
;
color
:
#8a5a12
;
background
:
#fff8e7
;
border
:
1px
solid
#f4dfad
;
border-radius
:
9px
;
font-size
:
10px
;
line-height
:
1.6
;
}
.form-warning
svg
{
flex
:
none
;
margin-top
:
1px
;
}
@media
(
max-width
:
800px
)
{
.template-grid
,
.create-parameter-grid
{
grid-template-columns
:
1
fr
;
}
.theme-options
{
grid-template-columns
:
repeat
(
2
,
1
fr
);
}
}
frontend/src/main.tsx
View file @
61c4a3b5
...
...
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
import
{
QueryClient
,
QueryClientProvider
}
from
"@tanstack/react-query"
;
import
App
from
"./App"
;
import
"./styles.css"
;
import
"./create-template.css"
;
const
queryClient
=
new
QueryClient
({
defaultOptions
:
{
queries
:
{
retry
:
1
,
refetchOnWindowFocus
:
false
}
}
});
...
...
shared/src/index.ts
View file @
61c4a3b5
...
...
@@ -10,6 +10,57 @@ export interface CreateSiteInput {
email
:
string
;
phone
?:
string
;
brandColor
:
string
;
pageTemplateId
:
string
;
pageTemplateVersion
:
string
;
themeId
:
string
;
layout
:
SiteLayoutParameters
;
initialPrompt
?:
string
;
}
export
interface
SiteLayoutParameters
{
hero
:
"split"
|
"centered"
;
density
:
"comfortable"
|
"compact"
;
radius
:
"soft"
|
"square"
;
}
export
interface
ThemePresetInfo
{
id
:
string
;
name
:
string
;
colors
:
string
[];
}
export
interface
PageTemplateInfo
{
id
:
string
;
version
:
string
;
name
:
string
;
description
:
string
;
industries
:
string
[];
preview
:
{
accent
:
string
;
surface
:
string
;
mode
:
"light"
|
"dark"
};
defaultThemeId
:
string
;
themeIds
:
string
[];
layouts
:
{
hero
:
SiteLayoutParameters
[
"hero"
][];
density
:
SiteLayoutParameters
[
"density"
][];
radius
:
SiteLayoutParameters
[
"radius"
][];
};
defaults
:
SiteLayoutParameters
;
}
export
interface
SiteTemplateCatalog
{
kernel
:
{
id
:
string
;
version
:
string
};
pageTemplates
:
PageTemplateInfo
[];
themes
:
ThemePresetInfo
[];
}
export
interface
SiteTemplateProvenance
{
kernel
:
{
id
:
string
;
version
:
string
};
createdFrom
:
{
pageTemplateId
:
string
;
pageTemplateVersion
:
string
;
themeId
:
string
;
layout
:
SiteLayoutParameters
;
};
initialPrompt
?:
string
;
}
export
interface
SiteInfo
{
...
...
@@ -19,6 +70,7 @@ export interface SiteInfo {
industry
:
string
;
status
:
SiteStatus
;
templateVersion
:
string
;
template
?:
SiteTemplateProvenance
;
environmentVersion
?:
number
;
previewPort
:
number
;
previewUrl
:
string
;
...
...
site-kit/kernel/manifest.json
0 → 100644
View file @
61c4a3b5
{
"id"
:
"official-site"
,
"version"
:
"1.0.0"
,
"status"
:
"stable"
,
"source"
:
"../../astro-template"
}
site-kit/page-templates/brand-story/1.0.0/manifest.json
0 → 100644
View file @
61c4a3b5
{
"id"
:
"brand-story"
,
"version"
:
"1.0.0"
,
"name"
:
"品牌故事企业"
,
"description"
:
"更有温度的叙事与舒展版式,适合消费品牌、文化创意和设计机构。"
,
"industries"
:
[
"消费品牌"
,
"文化创意"
,
"设计机构"
],
"status"
:
"published"
,
"compatibleKernel"
:
"1.x"
,
"preview"
:
{
"accent"
:
"#C2410C"
,
"surface"
:
"#FFF7ED"
,
"mode"
:
"light"
},
"defaultThemeId"
:
"warm-amber"
,
"themeIds"
:
[
"warm-amber"
,
"graphite"
,
"forest-green"
,
"violet-tech"
],
"layouts"
:
{
"hero"
:
[
"centered"
,
"split"
],
"density"
:
[
"comfortable"
,
"compact"
],
"radius"
:
[
"soft"
,
"square"
]
},
"defaults"
:
{
"hero"
:
"centered"
,
"density"
:
"comfortable"
,
"radius"
:
"soft"
},
"content"
:
{
"eyebrow"
:
"MADE WITH PURPOSE"
,
"heroTitle"
:
"把日常的灵感,
\n
做成值得记住的品牌"
,
"heroDescription"
:
"我们相信好品牌来自真实的价值、细腻的表达,以及与每一位用户长久相处的诚意。"
,
"primaryAction"
:
"认识我们的故事"
,
"secondaryAction"
:
"探索品牌作品"
,
"metrics"
:
[{
"value"
:
"36"
,
"label"
:
"城市足迹"
},{
"value"
:
"80万+"
,
"label"
:
"用户选择"
},{
"value"
:
"12项"
,
"label"
:
"设计荣誉"
}],
"advantages"
:
[{
"title"
:
"真实表达"
,
"description"
:
"从品牌本身出发,建立诚恳而鲜明的表达。"
},{
"title"
:
"细节体验"
,
"description"
:
"在每个接触点创造自然、统一的感受。"
},{
"title"
:
"长久陪伴"
,
"description"
:
"让品牌随着用户和时代持续成长。"
}]
}
}
site-kit/page-templates/professional-services/1.0.0/manifest.json
0 → 100644
View file @
61c4a3b5
{
"id"
:
"professional-services"
,
"version"
:
"1.0.0"
,
"name"
:
"专业服务企业"
,
"description"
:
"稳重克制、强调方法与信任,适合咨询、财税、法律和企业服务。"
,
"industries"
:
[
"管理咨询"
,
"法律财税"
,
"专业服务"
],
"status"
:
"published"
,
"compatibleKernel"
:
"1.x"
,
"preview"
:
{
"accent"
:
"#175CD3"
,
"surface"
:
"#EFF8FF"
,
"mode"
:
"light"
},
"defaultThemeId"
:
"ocean-blue"
,
"themeIds"
:
[
"ocean-blue"
,
"forest-green"
,
"graphite"
,
"violet-tech"
],
"layouts"
:
{
"hero"
:
[
"centered"
,
"split"
],
"density"
:
[
"comfortable"
,
"compact"
],
"radius"
:
[
"square"
,
"soft"
]
},
"defaults"
:
{
"hero"
:
"centered"
,
"density"
:
"comfortable"
,
"radius"
:
"square"
},
"content"
:
{
"eyebrow"
:
"TRUSTED PROFESSIONALS"
,
"heroTitle"
:
"以专业判断,
\n
回应复杂商业问题"
,
"heroDescription"
:
"从关键判断到可靠执行,我们与企业并肩工作,让每一项重要决策都有清晰依据。"
,
"primaryAction"
:
"预约顾问"
,
"secondaryAction"
:
"查看服务方法"
,
"metrics"
:
[{
"value"
:
"15年"
,
"label"
:
"行业经验"
},{
"value"
:
"300+"
,
"label"
:
"客户项目"
},{
"value"
:
"92%"
,
"label"
:
"长期合作客户"
}],
"advantages"
:
[{
"title"
:
"资深团队"
,
"description"
:
"由具备一线经验的专业顾问直接参与。"
},{
"title"
:
"事实驱动"
,
"description"
:
"基于充分研究和可靠数据形成判断。"
},{
"title"
:
"结果负责"
,
"description"
:
"从建议延伸到执行,确保方案真正落地。"
}]
}
}
site-kit/page-templates/technology-corporate/1.0.0/manifest.json
0 → 100644
View file @
61c4a3b5
{
"id"
:
"technology-corporate"
,
"version"
:
"1.0.0"
,
"name"
:
"现代科技企业"
,
"description"
:
"清晰有力的左右首屏与数据化视觉,适合 AI、软件和数字化服务企业。"
,
"industries"
:
[
"人工智能"
,
"企业软件"
,
"数字化服务"
],
"status"
:
"published"
,
"compatibleKernel"
:
"1.x"
,
"preview"
:
{
"accent"
:
"#7028FF"
,
"surface"
:
"#F3F0FF"
,
"mode"
:
"light"
},
"defaultThemeId"
:
"violet-tech"
,
"themeIds"
:
[
"violet-tech"
,
"ocean-blue"
,
"forest-green"
,
"graphite"
],
"layouts"
:
{
"hero"
:
[
"split"
,
"centered"
],
"density"
:
[
"comfortable"
,
"compact"
],
"radius"
:
[
"soft"
,
"square"
]
},
"defaults"
:
{
"hero"
:
"split"
,
"density"
:
"comfortable"
,
"radius"
:
"soft"
},
"content"
:
{
"eyebrow"
:
"AI-POWERED BUSINESS"
,
"heroTitle"
:
"让每一次数字化升级,
\n
都成为增长的起点"
,
"heroDescription"
:
"从策略洞察到产品落地,我们用清晰的方法与先进技术,为企业构建可持续的数字竞争力。"
,
"primaryAction"
:
"开始合作"
,
"secondaryAction"
:
"了解我们的能力"
,
"metrics"
:
[{
"value"
:
"120+"
,
"label"
:
"服务企业"
},{
"value"
:
"98%"
,
"label"
:
"项目交付率"
},{
"value"
:
"7×24"
,
"label"
:
"专业支持"
}],
"advantages"
:
[{
"title"
:
"业务优先"
,
"description"
:
"每一项方案都回应真实业务目标。"
},{
"title"
:
"敏捷共创"
,
"description"
:
"用短周期验证关键假设,让过程透明、结果可控。"
},{
"title"
:
"长期主义"
,
"description"
:
"交付可维护、可演进的系统,而不是一次性成果。"
}]
}
}
site-kit/themes.json
0 → 100644
View file @
61c4a3b5
[
{
"id"
:
"violet-tech"
,
"name"
:
"科技紫"
,
"colors"
:
[
"#7028FF"
,
"#536CFF"
,
"#F3F0FF"
],
"primary"
:
"#7028ff"
,
"hover"
:
"#5c1fe0"
,
"light"
:
"#8b5cff"
,
"secondary"
:
"#536cff"
,
"dark"
:
"#24105f"
,
"background"
:
"#f8f8fc"
,
"soft"
:
"#f3f0ff"
,
"text"
:
"#191927"
,
"textSecondary"
:
"#4f5260"
,
"muted"
:
"#8b8e99"
,
"border"
:
"#e7e3f3"
},
{
"id"
:
"ocean-blue"
,
"name"
:
"商务蓝"
,
"colors"
:
[
"#175CD3"
,
"#2E90FA"
,
"#EFF8FF"
],
"primary"
:
"#175cd3"
,
"hover"
:
"#144da8"
,
"light"
:
"#53b1fd"
,
"secondary"
:
"#2e90fa"
,
"dark"
:
"#102a56"
,
"background"
:
"#f7faff"
,
"soft"
:
"#eff8ff"
,
"text"
:
"#101828"
,
"textSecondary"
:
"#475467"
,
"muted"
:
"#98a2b3"
,
"border"
:
"#d1e9ff"
},
{
"id"
:
"forest-green"
,
"name"
:
"自然绿"
,
"colors"
:
[
"#087A5B"
,
"#12B76A"
,
"#ECFDF3"
],
"primary"
:
"#087a5b"
,
"hover"
:
"#066149"
,
"light"
:
"#32d583"
,
"secondary"
:
"#12b76a"
,
"dark"
:
"#12372d"
,
"background"
:
"#f8fbf9"
,
"soft"
:
"#ecfdf3"
,
"text"
:
"#16251f"
,
"textSecondary"
:
"#486158"
,
"muted"
:
"#8ca198"
,
"border"
:
"#ccebdc"
},
{
"id"
:
"warm-amber"
,
"name"
:
"品牌暖橙"
,
"colors"
:
[
"#C2410C"
,
"#F59E0B"
,
"#FFF7ED"
],
"primary"
:
"#c2410c"
,
"hover"
:
"#9a3412"
,
"light"
:
"#fb923c"
,
"secondary"
:
"#f59e0b"
,
"dark"
:
"#431407"
,
"background"
:
"#fffbf7"
,
"soft"
:
"#fff7ed"
,
"text"
:
"#29201b"
,
"textSecondary"
:
"#65544a"
,
"muted"
:
"#9f8d82"
,
"border"
:
"#fed7aa"
},
{
"id"
:
"graphite"
,
"name"
:
"黑金质感"
,
"colors"
:
[
"#B78932"
,
"#E4C477"
,
"#F6F2E8"
],
"primary"
:
"#9a7025"
,
"hover"
:
"#795719"
,
"light"
:
"#d6b45f"
,
"secondary"
:
"#b78932"
,
"dark"
:
"#1e1c18"
,
"background"
:
"#f8f6f1"
,
"soft"
:
"#f6f2e8"
,
"text"
:
"#1d1c19"
,
"textSecondary"
:
"#5e594f"
,
"muted"
:
"#969083"
,
"border"
:
"#ded7c7"
}
]
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment