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
9b34b595
Commit
9b34b595
authored
Jul 20, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add site management and recycle bin
parent
57609ddd
Changes
9
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
263 additions
and
11 deletions
+263
-11
config.ts
backend/src/config.ts
+1
-0
server.ts
backend/src/server.ts
+13
-1
site-lifecycle-service.ts
backend/src/sites/site-lifecycle-service.ts
+95
-0
site-repository.test.ts
backend/src/sites/site-repository.test.ts
+7
-1
site-repository.ts
backend/src/sites/site-repository.ts
+30
-0
App.tsx
frontend/src/App.tsx
+98
-9
api.ts
frontend/src/api.ts
+4
-0
styles.css
frontend/src/styles.css
+11
-0
index.ts
shared/src/index.ts
+4
-0
No files found.
backend/src/config.ts
View file @
9b34b595
...
@@ -90,6 +90,7 @@ export const config = {
...
@@ -90,6 +90,7 @@ export const config = {
export
const
runtimePaths
=
{
export
const
runtimePaths
=
{
sites
:
config
.
sitesDir
,
sites
:
config
.
sitesDir
,
archivedSites
:
path
.
join
(
config
.
sitesDir
,
".trash"
),
builds
:
path
.
join
(
config
.
runtimeDir
,
"builds"
),
builds
:
path
.
join
(
config
.
runtimeDir
,
"builds"
),
uploads
:
path
.
join
(
config
.
runtimeDir
,
"uploads"
),
uploads
:
path
.
join
(
config
.
runtimeDir
,
"uploads"
),
pnpmStore
:
path
.
join
(
config
.
runtimeDir
,
"pnpm-store"
),
pnpmStore
:
path
.
join
(
config
.
runtimeDir
,
"pnpm-store"
),
...
...
backend/src/server.ts
View file @
9b34b595
...
@@ -18,6 +18,7 @@ import { CreateSiteService } from "./sites/create-site.js";
...
@@ -18,6 +18,7 @@ import { CreateSiteService } from "./sites/create-site.js";
import
{
SiteAgentService
}
from
"./sites/site-agent-service.js"
;
import
{
SiteAgentService
}
from
"./sites/site-agent-service.js"
;
import
{
SiteRepository
}
from
"./sites/site-repository.js"
;
import
{
SiteRepository
}
from
"./sites/site-repository.js"
;
import
{
SiteVersionService
}
from
"./sites/site-version-service.js"
;
import
{
SiteVersionService
}
from
"./sites/site-version-service.js"
;
import
{
SiteLifecycleService
}
from
"./sites/site-lifecycle-service.js"
;
import
{
DomainDeploymentService
}
from
"./domains/domain-deployment-service.js"
;
import
{
DomainDeploymentService
}
from
"./domains/domain-deployment-service.js"
;
import
{
DomainRepository
}
from
"./domains/domain-repository.js"
;
import
{
DomainRepository
}
from
"./domains/domain-repository.js"
;
import
{
DomainRoutingConfig
}
from
"./domains/domain-routing-config.js"
;
import
{
DomainRoutingConfig
}
from
"./domains/domain-routing-config.js"
;
...
@@ -42,8 +43,10 @@ const siteAgent = new SiteAgentService(sites, git, new AgentLoop());
...
@@ -42,8 +43,10 @@ const siteAgent = new SiteAgentService(sites, git, new AgentLoop());
const
versions
=
new
SiteVersionService
(
sites
,
git
,
builds
,
previews
);
const
versions
=
new
SiteVersionService
(
sites
,
git
,
builds
,
previews
);
const
domainRepository
=
new
DomainRepository
();
const
domainRepository
=
new
DomainRepository
();
const
domainRouting
=
new
DomainRoutingConfig
(
domainRepository
);
const
domainRouting
=
new
DomainRoutingConfig
(
domainRepository
);
const
domainProvider
=
createDomainProvider
();
const
domainDeployments
=
new
DomainDeploymentService
(
sites
,
domainRepository
,
git
,
builds
,
domainRouting
);
const
domainDeployments
=
new
DomainDeploymentService
(
sites
,
domainRepository
,
git
,
builds
,
domainRouting
);
const
domainService
=
new
DomainService
(
sites
,
domainRepository
,
domainRouting
,
createDomainProvider
());
const
domainService
=
new
DomainService
(
sites
,
domainRepository
,
domainRouting
,
domainProvider
);
const
siteLifecycle
=
new
SiteLifecycleService
(
sites
,
previews
,
domainRepository
,
domainRouting
,
domainProvider
);
app
.
addHook
(
"onRequest"
,
async
(
request
,
reply
)
=>
{
app
.
addHook
(
"onRequest"
,
async
(
request
,
reply
)
=>
{
if
(
!
request
.
url
.
startsWith
(
"/api/"
)
||
request
.
url
===
"/api/health"
||
request
.
url
===
"/api/login"
)
return
;
if
(
!
request
.
url
.
startsWith
(
"/api/"
)
||
request
.
url
===
"/api/health"
||
request
.
url
===
"/api/login"
)
return
;
...
@@ -67,8 +70,15 @@ app.post("/api/logout", async (request) => {
...
@@ -67,8 +70,15 @@ app.post("/api/logout", async (request) => {
app
.
get
(
"/api/session"
,
async
(
request
)
=>
request
.
auth
!
);
app
.
get
(
"/api/session"
,
async
(
request
)
=>
request
.
auth
!
);
app
.
get
(
"/api/sites"
,
async
(
request
)
=>
sites
.
list
(
requireTenant
(
request
.
auth
)));
app
.
get
(
"/api/sites"
,
async
(
request
)
=>
sites
.
list
(
requireTenant
(
request
.
auth
)));
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
.
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
))));
app
.
post
(
"/api/sites"
,
async
(
request
,
reply
)
=>
reply
.
code
(
201
).
send
(
await
createSite
.
execute
(
requireTenant
(
request
.
auth
),
createSiteSchema
.
parse
(
request
.
body
))));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/archive"
,
async
(
request
)
=>
siteLifecycle
.
archive
(
requireTenant
(
request
.
auth
),
request
.
params
.
siteId
));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/archived-sites/:siteId/restore"
,
async
(
request
)
=>
siteLifecycle
.
restore
(
requireTenant
(
request
.
auth
),
request
.
params
.
siteId
));
app
.
delete
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/archived-sites/:siteId"
,
async
(
request
,
reply
)
=>
{
await
siteLifecycle
.
permanentlyDelete
(
requireTenant
(
request
.
auth
),
request
.
params
.
siteId
);
return
reply
.
code
(
204
).
send
();
});
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/chat"
,
async
(
request
)
=>
{
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/chat"
,
async
(
request
)
=>
{
const
body
=
chatSchema
.
parse
(
request
.
body
);
const
body
=
chatSchema
.
parse
(
request
.
body
);
return
siteAgent
.
execute
(
requireTenant
(
request
.
auth
),
request
.
params
.
siteId
,
body
.
message
);
return
siteAgent
.
execute
(
requireTenant
(
request
.
auth
),
request
.
params
.
siteId
,
body
.
message
);
...
@@ -168,6 +178,7 @@ app.setErrorHandler((error, _request, reply) => {
...
@@ -168,6 +178,7 @@ app.setErrorHandler((error, _request, reply) => {
});
});
await
sites
.
ensureRuntime
();
await
sites
.
ensureRuntime
();
await
siteLifecycle
.
purgeExpired
().
catch
((
error
)
=>
app
.
log
.
warn
(
error
));
await
domainRouting
.
sync
();
await
domainRouting
.
sync
();
await
rm
(
runtimePaths
.
builds
,
{
recursive
:
true
,
force
:
true
});
await
rm
(
runtimePaths
.
builds
,
{
recursive
:
true
,
force
:
true
});
await
mkdir
(
runtimePaths
.
builds
,
{
recursive
:
true
});
await
mkdir
(
runtimePaths
.
builds
,
{
recursive
:
true
});
...
@@ -219,6 +230,7 @@ const checkPendingDomains = async () => {
...
@@ -219,6 +230,7 @@ const checkPendingDomains = async () => {
}
finally
{
checkingDomains
=
false
;
}
}
finally
{
checkingDomains
=
false
;
}
};
};
setInterval
(()
=>
void
checkPendingDomains
(),
config
.
domains
.
checkIntervalMs
).
unref
();
setInterval
(()
=>
void
checkPendingDomains
(),
config
.
domains
.
checkIntervalMs
).
unref
();
setInterval
(()
=>
void
siteLifecycle
.
purgeExpired
().
catch
((
error
)
=>
app
.
log
.
warn
(
error
)),
24
*
60
*
60
*
1000
).
unref
();
function
bearerToken
(
header
:
string
|
undefined
):
string
|
undefined
{
function
bearerToken
(
header
:
string
|
undefined
):
string
|
undefined
{
const
match
=
header
?.
match
(
/^Bearer
\s
+
(
.+
)
$/i
);
const
match
=
header
?.
match
(
/^Bearer
\s
+
(
.+
)
$/i
);
...
...
backend/src/sites/site-lifecycle-service.ts
0 → 100644
View file @
9b34b595
import
path
from
"node:path"
;
import
{
mkdir
,
rename
,
rm
,
stat
}
from
"node:fs/promises"
;
import
type
{
SiteInfo
}
from
"@webagent/shared"
;
import
{
config
,
runtimePaths
}
from
"../config.js"
;
import
type
{
DomainProvider
}
from
"../domains/domain-provider.js"
;
import
{
DomainRepository
}
from
"../domains/domain-repository.js"
;
import
{
DomainRoutingConfig
}
from
"../domains/domain-routing-config.js"
;
import
{
PreviewProcessManager
}
from
"../preview/preview-process-manager.js"
;
import
{
SiteRepository
}
from
"./site-repository.js"
;
export
class
SiteLifecycleService
{
constructor
(
private
readonly
sites
:
SiteRepository
,
private
readonly
previews
:
PreviewProcessManager
,
private
readonly
domains
:
DomainRepository
,
private
readonly
routing
:
DomainRoutingConfig
,
private
readonly
provider
:
DomainProvider
,
)
{}
async
archive
(
tenantId
:
string
,
siteId
:
string
):
Promise
<
SiteInfo
>
{
const
site
=
await
this
.
sites
.
get
(
tenantId
,
siteId
);
const
archivedAt
=
new
Date
();
const
bindings
=
await
this
.
domains
.
list
(
tenantId
,
siteId
);
for
(
const
domain
of
bindings
)
await
this
.
provider
.
remove
(
domain
);
if
(
bindings
.
length
)
{
await
this
.
domains
.
updateSite
(
tenantId
,
siteId
,
()
=>
({
providerHostnameId
:
undefined
,
providerStatus
:
undefined
,
deploymentStatus
:
"pending"
,
deployedCommit
:
undefined
,
tlsStatus
:
this
.
provider
.
initialTlsStatus
(),
}));
}
await
this
.
sites
.
update
(
tenantId
,
siteId
,
{
lifecycleStatus
:
"archived"
,
archivedAt
:
archivedAt
.
toISOString
(),
deleteAfter
:
new
Date
(
archivedAt
.
getTime
()
+
30
*
24
*
60
*
60
*
1000
).
toISOString
(),
});
await
this
.
previews
.
stop
(
tenantId
,
siteId
);
await
this
.
move
(
this
.
sites
.
getSiteRoot
(
tenantId
,
siteId
),
this
.
sites
.
getArchivedSiteRoot
(
tenantId
,
siteId
),
true
);
await
Promise
.
all
(
this
.
artifactRoots
().
map
(({
active
,
archived
})
=>
this
.
move
(
path
.
join
(
active
,
tenantId
,
siteId
),
path
.
join
(
archived
,
tenantId
,
siteId
),
false
)));
await
this
.
routing
.
sync
();
return
this
.
sites
.
getArchived
(
tenantId
,
siteId
);
}
async
restore
(
tenantId
:
string
,
siteId
:
string
):
Promise
<
SiteInfo
>
{
await
this
.
sites
.
getArchived
(
tenantId
,
siteId
);
if
(
await
this
.
exists
(
this
.
sites
.
getSiteRoot
(
tenantId
,
siteId
)))
throw
Object
.
assign
(
new
Error
(
"已有同标识的网站,无法恢复"
),
{
statusCode
:
409
});
await
this
.
move
(
this
.
sites
.
getArchivedSiteRoot
(
tenantId
,
siteId
),
this
.
sites
.
getSiteRoot
(
tenantId
,
siteId
),
true
);
await
Promise
.
all
(
this
.
artifactRoots
().
map
(({
active
,
archived
})
=>
this
.
move
(
path
.
join
(
archived
,
tenantId
,
siteId
),
path
.
join
(
active
,
tenantId
,
siteId
),
false
)));
const
restored
=
await
this
.
sites
.
update
(
tenantId
,
siteId
,
{
lifecycleStatus
:
"active"
,
archivedAt
:
undefined
,
deleteAfter
:
undefined
});
const
previewPath
=
path
.
join
(
config
.
previewsDir
,
tenantId
,
siteId
);
if
(
await
this
.
exists
(
previewPath
))
{
await
this
.
previews
.
start
(
tenantId
,
siteId
,
previewPath
,
restored
.
previewPort
).
catch
(
async
(
error
)
=>
{
await
this
.
sites
.
update
(
tenantId
,
siteId
,
{
status
:
"failed"
,
lastError
:
error
instanceof
Error
?
error
.
message
:
String
(
error
)
});
});
}
await
this
.
routing
.
sync
();
return
this
.
sites
.
get
(
tenantId
,
siteId
);
}
async
permanentlyDelete
(
tenantId
:
string
,
siteId
:
string
):
Promise
<
void
>
{
await
this
.
sites
.
getArchived
(
tenantId
,
siteId
);
await
Promise
.
all
([
rm
(
this
.
sites
.
getArchivedSiteRoot
(
tenantId
,
siteId
),
{
recursive
:
true
,
force
:
true
}),
...
this
.
artifactRoots
().
flatMap
(({
active
,
archived
})
=>
[
rm
(
path
.
join
(
active
,
tenantId
,
siteId
),
{
recursive
:
true
,
force
:
true
}),
rm
(
path
.
join
(
archived
,
tenantId
,
siteId
),
{
recursive
:
true
,
force
:
true
}),
]),
]);
}
async
purgeExpired
(
now
=
new
Date
()):
Promise
<
number
>
{
const
expired
=
(
await
this
.
sites
.
listAllArchived
()).
filter
((
site
)
=>
site
.
deleteAfter
&&
new
Date
(
site
.
deleteAfter
).
getTime
()
<=
now
.
getTime
());
for
(
const
site
of
expired
)
await
this
.
permanentlyDelete
(
site
.
tenantId
,
site
.
siteId
);
return
expired
.
length
;
}
private
artifactRoots
():
Array
<
{
active
:
string
;
archived
:
string
}
>
{
return
[
config
.
previewsDir
,
config
.
productionDir
,
config
.
customDomainsDir
].
map
((
active
)
=>
({
active
,
archived
:
path
.
join
(
active
,
".trash"
)
}));
}
private
async
move
(
source
:
string
,
destination
:
string
,
required
:
boolean
):
Promise
<
void
>
{
if
(
!
await
this
.
exists
(
source
))
{
if
(
required
)
throw
Object
.
assign
(
new
Error
(
"站点不存在"
),
{
statusCode
:
404
});
return
;
}
await
mkdir
(
path
.
dirname
(
destination
),
{
recursive
:
true
});
await
rename
(
source
,
destination
);
}
private
async
exists
(
target
:
string
):
Promise
<
boolean
>
{
return
stat
(
target
).
then
(()
=>
true
).
catch
(()
=>
false
);
}
}
backend/src/sites/site-repository.test.ts
View file @
9b34b595
import
assert
from
"node:assert/strict"
;
import
assert
from
"node:assert/strict"
;
import
{
mkd
temp
,
rm
}
from
"node:fs/promises"
;
import
{
mkd
ir
,
mkdtemp
,
rename
,
rm
}
from
"node:fs/promises"
;
import
os
from
"node:os"
;
import
os
from
"node:os"
;
import
path
from
"node:path"
;
import
path
from
"node:path"
;
import
test
from
"node:test"
;
import
test
from
"node:test"
;
...
@@ -32,5 +32,11 @@ test("repository paths and queries are tenant-scoped", async () => {
...
@@ -32,5 +32,11 @@ test("repository paths and queries are tenant-scoped", async () => {
await
assert
.
rejects
(()
=>
sites
.
get
(
tenantA
,
siteB
),
(
error
:
NodeJS
.
ErrnoException
)
=>
error
.
code
===
"ENOENT"
);
await
assert
.
rejects
(()
=>
sites
.
get
(
tenantA
,
siteB
),
(
error
:
NodeJS
.
ErrnoException
)
=>
error
.
code
===
"ENOENT"
);
assert
.
equal
(
sites
.
getProjectPath
(
tenantA
,
siteA
),
path
.
join
(
directory
,
"sites"
,
tenantA
,
siteA
,
"project"
));
assert
.
equal
(
sites
.
getProjectPath
(
tenantA
,
siteA
),
path
.
join
(
directory
,
"sites"
,
tenantA
,
siteA
,
"project"
));
assert
.
notEqual
(
sites
.
getSiteRoot
(
tenantA
,
siteA
),
sites
.
getSiteRoot
(
tenantB
,
siteA
));
assert
.
notEqual
(
sites
.
getSiteRoot
(
tenantA
,
siteA
),
sites
.
getSiteRoot
(
tenantB
,
siteA
));
await
mkdir
(
path
.
dirname
(
sites
.
getArchivedSiteRoot
(
tenantA
,
siteA
)),
{
recursive
:
true
});
await
rename
(
sites
.
getSiteRoot
(
tenantA
,
siteA
),
sites
.
getArchivedSiteRoot
(
tenantA
,
siteA
));
assert
.
deepEqual
(
await
sites
.
list
(
tenantA
),
[]);
assert
.
deepEqual
((
await
sites
.
listArchived
(
tenantA
)).
map
((
site
)
=>
site
.
siteId
),
[
siteA
]);
assert
.
equal
((
await
sites
.
getArchived
(
tenantA
,
siteA
)).
lifecycleStatus
,
"archived"
);
}
finally
{
await
rm
(
directory
,
{
recursive
:
true
,
force
:
true
});
}
}
finally
{
await
rm
(
directory
,
{
recursive
:
true
,
force
:
true
});
}
});
});
backend/src/sites/site-repository.ts
View file @
9b34b595
...
@@ -22,6 +22,12 @@ export class SiteRepository {
...
@@ -22,6 +22,12 @@ export class SiteRepository {
return
path
.
join
(
runtimePaths
.
sites
,
tenantId
,
siteId
);
return
path
.
join
(
runtimePaths
.
sites
,
tenantId
,
siteId
);
}
}
getArchivedSiteRoot
(
tenantId
:
string
,
siteId
:
string
):
string
{
this
.
assertTenantId
(
tenantId
);
this
.
assertSiteId
(
siteId
);
return
path
.
join
(
runtimePaths
.
archivedSites
,
tenantId
,
siteId
);
}
getProjectPath
(
tenantId
:
string
,
siteId
:
string
):
string
{
getProjectPath
(
tenantId
:
string
,
siteId
:
string
):
string
{
return
path
.
join
(
this
.
getSiteRoot
(
tenantId
,
siteId
),
"project"
);
return
path
.
join
(
this
.
getSiteRoot
(
tenantId
,
siteId
),
"project"
);
}
}
...
@@ -56,6 +62,30 @@ export class SiteRepository {
...
@@ -56,6 +62,30 @@ export class SiteRepository {
return
results
.
filter
((
site
):
site
is
SiteInfo
=>
site
!==
null
).
sort
((
a
,
b
)
=>
b
.
updatedAt
.
localeCompare
(
a
.
updatedAt
));
return
results
.
filter
((
site
):
site
is
SiteInfo
=>
site
!==
null
).
sort
((
a
,
b
)
=>
b
.
updatedAt
.
localeCompare
(
a
.
updatedAt
));
}
}
async
getArchived
(
tenantId
:
string
,
siteId
:
string
):
Promise
<
SiteInfo
>
{
const
raw
=
await
readFile
(
path
.
join
(
this
.
getArchivedSiteRoot
(
tenantId
,
siteId
),
"metadata"
,
"site.json"
),
"utf8"
);
const
stored
=
JSON
.
parse
(
raw
)
as
SiteInfo
;
if
(
stored
.
tenantId
!==
tenantId
||
stored
.
siteId
!==
siteId
)
throw
Object
.
assign
(
new
Error
(
"站点不存在"
),
{
statusCode
:
404
});
return
{
...
stored
,
lifecycleStatus
:
"archived"
,
previewUrl
:
""
,
productionUrl
:
undefined
};
}
async
listArchived
(
tenantId
:
string
):
Promise
<
SiteInfo
[]
>
{
await
this
.
ensureRuntime
();
const
root
=
path
.
join
(
runtimePaths
.
archivedSites
,
tenantId
);
const
entries
=
await
readdir
(
root
,
{
withFileTypes
:
true
}).
catch
(()
=>
[]);
const
results
=
await
Promise
.
all
(
entries
.
filter
((
entry
)
=>
entry
.
isDirectory
()
&&
/^site_
[
a-z0-9
]
+$/
.
test
(
entry
.
name
)).
map
(
async
(
entry
)
=>
{
try
{
return
await
this
.
getArchived
(
tenantId
,
entry
.
name
);
}
catch
{
return
null
;
}
}));
return
results
.
filter
((
site
):
site
is
SiteInfo
=>
site
!==
null
).
sort
((
a
,
b
)
=>
(
b
.
archivedAt
||
b
.
updatedAt
).
localeCompare
(
a
.
archivedAt
||
a
.
updatedAt
));
}
async
listAllArchived
():
Promise
<
SiteInfo
[]
>
{
await
this
.
ensureRuntime
();
const
tenants
=
await
readdir
(
runtimePaths
.
archivedSites
,
{
withFileTypes
:
true
}).
catch
(()
=>
[]);
const
groups
=
await
Promise
.
all
(
tenants
.
filter
((
entry
)
=>
entry
.
isDirectory
()
&&
/^tenant_
[
a-z0-9_
]
+$/
.
test
(
entry
.
name
)).
map
((
entry
)
=>
this
.
listArchived
(
entry
.
name
)));
return
groups
.
flat
().
sort
((
a
,
b
)
=>
(
b
.
archivedAt
||
b
.
updatedAt
).
localeCompare
(
a
.
archivedAt
||
a
.
updatedAt
));
}
async
listAll
():
Promise
<
SiteInfo
[]
>
{
async
listAll
():
Promise
<
SiteInfo
[]
>
{
await
this
.
ensureRuntime
();
await
this
.
ensureRuntime
();
const
tenants
=
await
readdir
(
runtimePaths
.
sites
,
{
withFileTypes
:
true
}).
catch
(()
=>
[]);
const
tenants
=
await
readdir
(
runtimePaths
.
sites
,
{
withFileTypes
:
true
}).
catch
(()
=>
[]);
...
...
frontend/src/App.tsx
View file @
9b34b595
This diff is collapsed.
Click to expand it.
frontend/src/api.ts
View file @
9b34b595
...
@@ -24,8 +24,12 @@ export const api = {
...
@@ -24,8 +24,12 @@ export const api = {
session
:
()
=>
request
<
SessionInfo
>
(
"/api/session"
),
session
:
()
=>
request
<
SessionInfo
>
(
"/api/session"
),
health
:
()
=>
request
<
{
status
:
string
;
agentMode
:
"model"
|
"local"
}
>
(
"/api/health"
),
health
:
()
=>
request
<
{
status
:
string
;
agentMode
:
"model"
|
"local"
}
>
(
"/api/health"
),
sites
:
()
=>
request
<
SiteInfo
[]
>
(
"/api/sites"
),
sites
:
()
=>
request
<
SiteInfo
[]
>
(
"/api/sites"
),
archivedSites
:
()
=>
request
<
SiteInfo
[]
>
(
"/api/archived-sites"
),
site
:
(
siteId
:
string
)
=>
request
<
SiteInfo
>
(
"/api/sites/"
+
siteId
),
site
:
(
siteId
:
string
)
=>
request
<
SiteInfo
>
(
"/api/sites/"
+
siteId
),
createSite
:
(
input
:
CreateSiteInput
)
=>
request
<
SiteInfo
>
(
"/api/sites"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
(
input
)
}),
createSite
:
(
input
:
CreateSiteInput
)
=>
request
<
SiteInfo
>
(
"/api/sites"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
(
input
)
}),
archiveSite
:
(
siteId
:
string
)
=>
request
<
SiteInfo
>
(
"/api/sites/"
+
siteId
+
"/archive"
,
{
method
:
"POST"
}),
restoreSite
:
(
siteId
:
string
)
=>
request
<
SiteInfo
>
(
"/api/archived-sites/"
+
siteId
+
"/restore"
,
{
method
:
"POST"
}),
deleteSite
:
(
siteId
:
string
)
=>
request
<
void
>
(
"/api/archived-sites/"
+
siteId
,
{
method
:
"DELETE"
}),
chat
:
(
siteId
:
string
,
message
:
string
)
=>
request
<
ChatResult
>
(
"/api/sites/"
+
siteId
+
"/chat"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
message
})
}),
chat
:
(
siteId
:
string
,
message
:
string
)
=>
request
<
ChatResult
>
(
"/api/sites/"
+
siteId
+
"/chat"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
message
})
}),
history
:
(
siteId
:
string
)
=>
request
<
GitHistoryItem
[]
>
(
"/api/sites/"
+
siteId
+
"/history"
),
history
:
(
siteId
:
string
)
=>
request
<
GitHistoryItem
[]
>
(
"/api/sites/"
+
siteId
+
"/history"
),
previewVersion
:
(
siteId
:
string
,
commit
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/preview-version"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
commit
})
}),
previewVersion
:
(
siteId
:
string
,
commit
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/preview-version"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
commit
})
}),
...
...
frontend/src/styles.css
View file @
9b34b595
This diff is collapsed.
Click to expand it.
shared/src/index.ts
View file @
9b34b595
export
type
SiteStatus
=
"creating"
|
"ready"
|
"building"
|
"failed"
;
export
type
SiteStatus
=
"creating"
|
"ready"
|
"building"
|
"failed"
;
export
type
PublishStatus
=
"unpublished"
|
"publishing"
|
"published"
|
"failed"
;
export
type
PublishStatus
=
"unpublished"
|
"publishing"
|
"published"
|
"failed"
;
export
type
SiteLifecycleStatus
=
"active"
|
"archived"
;
export
interface
CreateSiteInput
{
export
interface
CreateSiteInput
{
name
:
string
;
name
:
string
;
...
@@ -35,6 +36,9 @@ export interface SiteInfo {
...
@@ -35,6 +36,9 @@ export interface SiteInfo {
updatedAt
:
string
;
updatedAt
:
string
;
lastError
?:
string
;
lastError
?:
string
;
lastPublishError
?:
string
;
lastPublishError
?:
string
;
lifecycleStatus
?:
SiteLifecycleStatus
;
archivedAt
?:
string
;
deleteAfter
?:
string
;
}
}
export
type
AccountType
=
"system_admin"
|
"tenant_user"
;
export
type
AccountType
=
"system_admin"
|
"tenant_user"
;
...
...
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