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
f86dd1a6
Commit
f86dd1a6
authored
Jul 15, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add authenticated workspace drafts
parent
10c7988e
Changes
14
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
280 additions
and
103 deletions
+280
-103
.env.example
.env.example
+2
-0
build-manager.ts
backend/src/build/build-manager.ts
+1
-1
config.ts
backend/src/config.ts
+4
-0
git-manager.ts
backend/src/git/git-manager.ts
+25
-0
schemas.ts
backend/src/schemas.ts
+5
-0
server.ts
backend/src/server.ts
+27
-2
site-agent-service.ts
backend/src/sites/site-agent-service.ts
+16
-38
site-repository.ts
backend/src/sites/site-repository.ts
+4
-0
site-version-service.ts
backend/src/sites/site-version-service.ts
+32
-0
storage-convention.md
docs/storage-convention.md
+4
-3
App.tsx
frontend/src/App.tsx
+138
-51
api.ts
frontend/src/api.ts
+6
-0
styles.css
frontend/src/styles.css
+10
-6
index.ts
shared/src/index.ts
+6
-2
No files found.
.env.example
View file @
f86dd1a6
PORT=3100
HOST=127.0.0.1
FRONTEND_ORIGIN=http://localhost
# 单用户登录账号固定为 user;生产环境请务必修改默认密码。
WEBAGENT_USER_PASSWORD=user
# 相对路径以 WebAgent 项目根目录为基准,也支持 /data/WebAgent-sites 等绝对路径。
WEBAGENT_SITES_DIR=../WebAgent-sites
# 已上线的静态产物独立保存,不会被测试环境构建覆盖。
...
...
backend/src/build/build-manager.ts
View file @
f86dd1a6
...
...
@@ -20,7 +20,7 @@ export class BuildManager {
output
+=
install
.
stdout
+
"
\n
"
+
install
.
stderr
+
"
\n
"
;
const
build
=
await
execa
(
"pnpm"
,
[
"run"
,
"build"
],
{
cwd
:
projectPath
,
env
:
{
...
process
.
env
,
SITE_BASE_PATH
:
basePath
.
replace
(
/
\/
$/
,
""
)
||
"/"
},
env
:
{
...
process
.
env
,
CI
:
"true"
,
SITE_BASE_PATH
:
basePath
.
replace
(
/
\/
$/
,
""
)
||
"/"
},
});
output
+=
build
.
stdout
+
"
\n
"
+
build
.
stderr
;
await
writeFile
(
path
.
join
(
runtimePaths
.
logs
,
taskId
+
".log"
),
output
,
"utf8"
);
...
...
backend/src/config.ts
View file @
f86dd1a6
...
...
@@ -22,6 +22,10 @@ export const config = {
host
:
process
.
env
.
HOST
||
"127.0.0.1"
,
port
:
Number
(
process
.
env
.
PORT
||
3100
),
frontendOrigin
:
process
.
env
.
FRONTEND_ORIGIN
||
"http://localhost:5173"
,
auth
:
{
username
:
"user"
,
password
:
process
.
env
.
WEBAGENT_USER_PASSWORD
||
"user"
,
},
openai
:
{
apiKey
:
process
.
env
.
OPENAI_API_KEY
||
""
,
baseUrl
:
(
process
.
env
.
OPENAI_BASE_URL
||
"https://api.deepseek.com/v1"
).
replace
(
/
\/
$/
,
""
),
...
...
backend/src/git/git-manager.ts
View file @
f86dd1a6
...
...
@@ -45,6 +45,23 @@ export class GitManager {
await
execa
(
"git"
,
[
"worktree"
,
"add"
,
"--detach"
,
workspacePath
,
reference
],
{
cwd
:
projectPath
});
}
async
createPersistentWorktree
(
projectPath
:
string
,
workspacePath
:
string
,
reference
=
"HEAD"
):
Promise
<
void
>
{
await
this
.
removePersistentWorktree
(
projectPath
,
workspacePath
);
await
mkdir
(
path
.
dirname
(
workspacePath
),
{
recursive
:
true
});
await
execa
(
"git"
,
[
"worktree"
,
"add"
,
"--detach"
,
workspacePath
,
reference
],
{
cwd
:
projectPath
});
}
async
removePersistentWorktree
(
projectPath
:
string
,
workspacePath
:
string
):
Promise
<
void
>
{
await
execa
(
"git"
,
[
"worktree"
,
"remove"
,
"--force"
,
workspacePath
],
{
cwd
:
projectPath
,
reject
:
false
});
await
execa
(
"git"
,
[
"worktree"
,
"prune"
],
{
cwd
:
projectPath
,
reject
:
false
});
await
rm
(
workspacePath
,
{
recursive
:
true
,
force
:
true
});
}
async
hasChanges
(
workspacePath
:
string
):
Promise
<
boolean
>
{
const
result
=
await
execa
(
"git"
,
[
"status"
,
"--porcelain"
],
{
cwd
:
workspacePath
});
return
result
.
stdout
.
trim
().
length
>
0
;
}
async
removeWorktree
(
projectPath
:
string
,
workspacePath
:
string
):
Promise
<
void
>
{
await
execa
(
"git"
,
[
"worktree"
,
"remove"
,
"--force"
,
workspacePath
],
{
cwd
:
projectPath
,
reject
:
false
});
await
execa
(
"git"
,
[
"worktree"
,
"prune"
],
{
cwd
:
projectPath
,
reject
:
false
});
...
...
@@ -69,6 +86,14 @@ export class GitManager {
return
this
.
currentCommit
(
projectPath
);
}
async
fastForward
(
projectPath
:
string
,
commit
:
string
,
expectedBase
:
string
):
Promise
<
string
>
{
const
current
=
await
this
.
currentCommit
(
projectPath
);
if
(
current
===
commit
)
return
current
;
if
(
current
!==
expectedBase
)
throw
new
Error
(
"最近保存版本已变化,请刷新后重新预览草稿"
);
await
execa
(
"git"
,
[
"merge"
,
"--ff-only"
,
commit
],
{
cwd
:
projectPath
});
return
this
.
currentCommit
(
projectPath
);
}
async
restoreAsCommit
(
projectPath
:
string
,
sourceCommit
:
string
):
Promise
<
string
>
{
await
this
.
assertCommit
(
projectPath
,
sourceCommit
);
await
execa
(
"git"
,
[
"restore"
,
"--source"
,
sourceCommit
,
"--staged"
,
"--worktree"
,
"--"
,
"."
,
":(exclude)astro.config.mjs"
],
{
cwd
:
projectPath
});
...
...
backend/src/schemas.ts
View file @
f86dd1a6
...
...
@@ -10,6 +10,11 @@ export const createSiteSchema = z.object({
brandColor
:
z
.
string
().
regex
(
/^#
[
0-9a-fA-F
]{6}
$/
,
"品牌色必须是 6 位十六进制颜色"
),
});
export
const
loginSchema
=
z
.
object
({
username
:
z
.
string
().
trim
().
min
(
1
),
password
:
z
.
string
().
min
(
1
),
});
export
const
chatSchema
=
z
.
object
({
message
:
z
.
string
().
trim
().
min
(
2
).
max
(
2000
),
});
...
...
backend/src/server.ts
View file @
f86dd1a6
import
path
from
"node:path"
;
import
crypto
from
"node:crypto"
;
import
{
access
,
mkdir
,
rm
}
from
"node:fs/promises"
;
import
Fastify
from
"fastify"
;
import
cors
from
"@fastify/cors"
;
...
...
@@ -8,7 +9,7 @@ import { BuildManager } from "./build/build-manager.js";
import
{
config
,
runtimePaths
}
from
"./config.js"
;
import
{
GitManager
}
from
"./git/git-manager.js"
;
import
{
PreviewProcessManager
}
from
"./preview/preview-process-manager.js"
;
import
{
chatSchema
,
createSiteSchema
,
versionCommitSchema
}
from
"./schemas.js"
;
import
{
chatSchema
,
createSiteSchema
,
loginSchema
,
versionCommitSchema
}
from
"./schemas.js"
;
import
{
CreateSiteService
}
from
"./sites/create-site.js"
;
import
{
SiteAgentService
}
from
"./sites/site-agent-service.js"
;
import
{
SiteRepository
}
from
"./sites/site-repository.js"
;
...
...
@@ -22,10 +23,33 @@ const git = new GitManager();
const
builds
=
new
BuildManager
();
const
previews
=
new
PreviewProcessManager
();
const
createSite
=
new
CreateSiteService
(
sites
,
git
,
builds
,
previews
);
const
siteAgent
=
new
SiteAgentService
(
sites
,
git
,
builds
,
previews
,
new
AgentLoop
());
const
siteAgent
=
new
SiteAgentService
(
sites
,
git
,
new
AgentLoop
());
const
versions
=
new
SiteVersionService
(
sites
,
git
,
builds
,
previews
);
const
sessions
=
new
Set
<
string
>
();
app
.
addHook
(
"onRequest"
,
async
(
request
,
reply
)
=>
{
if
(
!
request
.
url
.
startsWith
(
"/api/"
)
||
request
.
url
===
"/api/health"
||
request
.
url
===
"/api/login"
)
return
;
const
token
=
request
.
headers
.
authorization
?.
replace
(
/^Bearer
\s
+/i
,
""
);
if
(
!
token
||
!
sessions
.
has
(
token
))
return
reply
.
code
(
401
).
send
({
error
:
"登录已失效,请重新登录"
});
});
app
.
get
(
"/api/health"
,
async
()
=>
({
status
:
"ok"
,
service
:
"WebAgent"
,
agentMode
:
config
.
openai
.
apiKey
?
"model"
:
"local"
,
timestamp
:
new
Date
().
toISOString
()
}));
app
.
post
(
"/api/login"
,
async
(
request
,
reply
)
=>
{
const
body
=
loginSchema
.
parse
(
request
.
body
);
const
digest
=
(
value
:
string
)
=>
crypto
.
createHash
(
"sha256"
).
update
(
value
).
digest
();
const
validUsername
=
crypto
.
timingSafeEqual
(
digest
(
body
.
username
),
digest
(
config
.
auth
.
username
));
const
validPassword
=
crypto
.
timingSafeEqual
(
digest
(
body
.
password
),
digest
(
config
.
auth
.
password
));
if
(
!
validUsername
||
!
validPassword
)
return
reply
.
code
(
401
).
send
({
error
:
"账号或密码错误"
});
const
token
=
crypto
.
randomBytes
(
32
).
toString
(
"hex"
);
sessions
.
add
(
token
);
return
{
token
,
username
:
config
.
auth
.
username
};
});
app
.
post
(
"/api/logout"
,
async
(
request
)
=>
{
const
token
=
request
.
headers
.
authorization
?.
replace
(
/^Bearer
\s
+/i
,
""
);
if
(
token
)
sessions
.
delete
(
token
);
return
{
success
:
true
};
});
app
.
get
(
"/api/session"
,
async
()
=>
({
username
:
config
.
auth
.
username
}));
app
.
get
(
"/api/sites"
,
async
()
=>
sites
.
list
());
app
.
get
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId"
,
async
(
request
)
=>
sites
.
get
(
request
.
params
.
siteId
));
app
.
post
(
"/api/sites"
,
async
(
request
,
reply
)
=>
reply
.
code
(
201
).
send
(
await
createSite
.
execute
(
createSiteSchema
.
parse
(
request
.
body
))));
...
...
@@ -36,6 +60,7 @@ app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/build", async (requ
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/preview-version"
,
async
(
request
)
=>
{
const
body
=
versionCommitSchema
.
parse
(
request
.
body
);
return
versions
.
previewVersion
(
request
.
params
.
siteId
,
body
.
commit
);
});
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/preview-draft"
,
async
(
request
)
=>
versions
.
previewDraft
(
request
.
params
.
siteId
));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/publish"
,
async
(
request
)
=>
versions
.
publish
(
request
.
params
.
siteId
));
app
.
get
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/history"
,
async
(
request
)
=>
git
.
history
(
sites
.
getProjectPath
(
request
.
params
.
siteId
)));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/undo"
,
async
(
request
)
=>
versions
.
undo
(
request
.
params
.
siteId
));
...
...
backend/src/sites/site-agent-service.ts
View file @
f86dd1a6
import
crypto
from
"node:crypto"
;
import
path
from
"node:path"
;
import
type
{
ChatResult
,
SitePatch
}
from
"@webagent/shared"
;
import
{
getPublicPreviewUrl
,
runtimePaths
}
from
"../config.js"
;
import
{
AgentLoop
}
from
"../agent/agent-loop.js"
;
import
{
BuildError
,
BuildManager
}
from
"../build/build-manager.js"
;
import
{
GitManager
}
from
"../git/git-manager.js"
;
import
{
PreviewProcessManager
}
from
"../preview/preview-process-manager.js"
;
import
{
validatePatch
}
from
"../security/path-policy.js"
;
import
{
applyPatch
}
from
"./apply-patch.js"
;
import
{
SiteRepository
}
from
"./site-repository.js"
;
...
...
@@ -13,56 +8,39 @@ import { SiteRepository } from "./site-repository.js";
export
class
SiteAgentService
{
constructor
(
private
readonly
sites
:
SiteRepository
,
private
readonly
git
:
GitManager
,
private
readonly
builds
:
BuildManager
,
private
readonly
previews
:
PreviewProcessManager
,
private
readonly
agent
:
AgentLoop
,
)
{}
async
execute
(
siteId
:
string
,
message
:
string
):
Promise
<
ChatResult
>
{
const
site
=
await
this
.
sites
.
get
(
siteId
);
if
(
site
.
previewCommit
&&
site
.
previewCommit
!==
site
.
currentCommit
)
{
throw
new
Error
(
"当前测试环境正在预览历史版本,请先切换到最近保存版本再继续修改"
);
}
const
projectPath
=
this
.
sites
.
getProjectPath
(
siteId
);
const
taskId
=
"task_"
+
crypto
.
randomBytes
(
4
).
toString
(
"hex"
);
const
workspace
=
path
.
join
(
runtimePaths
.
builds
,
taskId
,
"workspace"
);
const
workspace
=
this
.
sites
.
getDraftPath
(
siteId
);
const
baseCommit
=
site
.
draftBaseCommit
||
site
.
currentCommit
;
const
creatingDraft
=
!
site
.
draftBaseCommit
;
await
this
.
sites
.
update
(
siteId
,
{
status
:
"building"
,
lastError
:
undefined
});
let
generated
:
{
patch
:
SitePatch
;
mode
:
"model"
|
"local"
}
|
undefined
;
try
{
await
this
.
git
.
createWorktree
(
projectPath
,
workspace
);
if
(
creatingDraft
)
await
this
.
git
.
createPersistentWorktree
(
projectPath
,
workspace
,
baseCommit
);
generated
=
await
this
.
agent
.
generate
(
workspace
,
message
);
await
validatePatch
(
generated
.
patch
,
workspace
);
await
applyPatch
(
workspace
,
generated
.
patch
);
let
dist
:
string
;
try
{
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
getPublicPreviewUrl
(
siteId
));
}
catch
(
error
)
{
if
(
!
(
error
instanceof
BuildError
)
||
generated
.
mode
!==
"model"
)
throw
error
;
const
repair
=
await
this
.
agent
.
repair
(
workspace
,
message
,
error
.
output
);
await
validatePatch
(
repair
,
workspace
);
await
applyPatch
(
workspace
,
repair
);
const
mergedOperations
=
new
Map
(
[...
generated
.
patch
.
operations
,
...
repair
.
operations
].
map
((
operation
)
=>
[
operation
.
path
,
operation
]),
);
if
(
mergedOperations
.
size
>
5
)
throw
new
Error
(
"自动修复后的修改文件总数超过 5 个,任务已安全取消"
);
generated
.
patch
=
{
summary
:
repair
.
summary
,
operations
:
[...
mergedOperations
.
values
()]
};
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
+
"_repair"
,
getPublicPreviewUrl
(
siteId
));
}
const
changedFiles
=
generated
.
patch
.
operations
.
map
((
item
)
=>
item
.
path
);
const
worktreeCommit
=
await
this
.
git
.
commitWorktree
(
workspace
,
"Agent: "
+
generated
.
patch
.
summary
,
changedFiles
);
const
commit
=
await
this
.
git
.
cherryPick
(
projectPath
,
worktreeCommit
);
const
published
=
await
this
.
builds
.
publishPreview
(
siteId
,
dist
);
await
this
.
previews
.
start
(
siteId
,
published
,
site
.
previewPort
);
const
updatedAt
=
new
Date
().
toISOString
();
await
this
.
sites
.
update
(
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
previewCommit
:
commi
t
,
previousCommit
:
site
.
currentCommit
,
environmentVersion
:
3
,
lastError
:
undefined
,
status
:
"ready"
,
draftBaseCommit
:
baseCommit
,
draftUpdatedAt
:
updatedA
t
,
draftSummary
:
generated
.
patch
.
summary
,
lastError
:
undefined
,
});
return
{
summary
:
generated
.
patch
.
summary
,
commit
,
previewUrl
:
site
.
previewUrl
,
changedFiles
,
mode
:
generated
.
mode
};
return
{
summary
:
generated
.
patch
.
summary
,
draft
:
true
,
baseCommit
,
updatedAt
,
changedFiles
,
mode
:
generated
.
mode
};
}
catch
(
error
)
{
const
details
=
error
instanceof
BuildError
?
error
.
output
.
slice
(
-
3000
)
:
error
instanceof
Error
?
error
.
message
:
String
(
error
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
site
.
previewCommit
?
"ready"
:
"failed"
,
lastError
:
details
});
const
details
=
error
instanceof
Error
?
error
.
message
:
String
(
error
);
const
keepDraft
=
!
creatingDraft
||
await
this
.
git
.
hasChanges
(
workspace
).
catch
(()
=>
false
);
if
(
!
keepDraft
)
await
this
.
git
.
removePersistentWorktree
(
projectPath
,
workspace
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
site
.
previewCommit
?
"ready"
:
"failed"
,
lastError
:
details
,
draftBaseCommit
:
keepDraft
?
baseCommit
:
undefined
,
draftUpdatedAt
:
keepDraft
?
new
Date
().
toISOString
()
:
undefined
,
});
throw
error
;
}
finally
{
await
this
.
git
.
removeWorktree
(
projectPath
,
workspace
);
}
}
}
backend/src/sites/site-repository.ts
View file @
f86dd1a6
...
...
@@ -20,6 +20,10 @@ export class SiteRepository {
return
path
.
join
(
this
.
getSiteRoot
(
siteId
),
"project"
);
}
getDraftPath
(
siteId
:
string
):
string
{
return
path
.
join
(
this
.
getSiteRoot
(
siteId
),
"draft"
);
}
getMetadataPath
(
siteId
:
string
):
string
{
return
path
.
join
(
this
.
getSiteRoot
(
siteId
),
"metadata"
,
"site.json"
);
}
...
...
backend/src/sites/site-version-service.ts
View file @
f86dd1a6
...
...
@@ -35,8 +35,40 @@ export class SiteVersionService {
return
{
commit
:
targetCommit
,
previewUrl
:
site
.
previewUrl
};
}
async
previewDraft
(
siteId
:
string
):
Promise
<
PreviewVersionResult
>
{
const
site
=
await
this
.
sites
.
get
(
siteId
);
if
(
!
site
.
draftBaseCommit
)
throw
new
Error
(
"当前没有可预览的工作草稿"
);
const
project
=
this
.
sites
.
getProjectPath
(
siteId
);
const
workspace
=
this
.
sites
.
getDraftPath
(
siteId
);
const
taskId
=
"draft_"
+
crypto
.
randomBytes
(
4
).
toString
(
"hex"
);
const
draftHead
=
await
this
.
git
.
currentCommit
(
workspace
);
if
(
draftHead
===
site
.
draftBaseCommit
&&
!
await
this
.
git
.
hasChanges
(
workspace
))
throw
new
Error
(
"工作草稿没有实际修改"
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
"building"
,
lastError
:
undefined
});
try
{
await
ensureEnvironmentConfig
(
workspace
);
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
getPublicPreviewUrl
(
siteId
));
const
draftCommit
=
await
this
.
git
.
commit
(
workspace
,
"Agent: "
+
(
site
.
draftSummary
||
"保存工作草稿"
));
const
commit
=
await
this
.
git
.
fastForward
(
project
,
draftCommit
,
site
.
draftBaseCommit
);
const
published
=
await
this
.
builds
.
publishPreview
(
siteId
,
dist
);
await
this
.
previews
.
start
(
siteId
,
published
,
site
.
previewPort
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
previewCommit
:
commit
,
previousCommit
:
site
.
currentCommit
,
draftBaseCommit
:
undefined
,
draftUpdatedAt
:
undefined
,
draftSummary
:
undefined
,
environmentVersion
:
3
,
lastError
:
undefined
,
});
await
this
.
git
.
removePersistentWorktree
(
project
,
workspace
);
return
{
commit
,
previewUrl
:
site
.
previewUrl
};
}
catch
(
error
)
{
const
details
=
error
instanceof
BuildError
?
error
.
output
.
slice
(
-
3000
)
:
error
instanceof
Error
?
error
.
message
:
String
(
error
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
site
.
previewCommit
?
"ready"
:
"failed"
,
lastError
:
details
});
throw
error
;
}
}
async
undo
(
siteId
:
string
):
Promise
<
PreviewVersionResult
>
{
const
site
=
await
this
.
sites
.
get
(
siteId
);
if
(
site
.
draftBaseCommit
)
throw
new
Error
(
"工作草稿尚未预览,请先预览草稿或继续编辑"
);
const
project
=
this
.
sites
.
getProjectPath
(
siteId
);
if
(
site
.
previewCommit
&&
site
.
previewCommit
!==
site
.
currentCommit
)
{
throw
new
Error
(
"当前正在查看历史版本,请先切换回最近保存版本再撤销修改"
);
...
...
docs/storage-convention.md
View file @
f86dd1a6
...
...
@@ -6,6 +6,7 @@
WebAgent-sites/
└── site_xxxxxx/
├── project/ # 独立 Astro 项目,也是独立 Git 仓库
├── draft/ # 持久化工作草稿 Git Worktree,不进入正式版本历史
└── metadata/
└── site.json # WebAgent 管理信息,不交给 Agent 修改
...
...
@@ -27,9 +28,9 @@ WebAgent/.runtime/
## 不变量
1.
Agent 只可读取和修改当前站点
`project/`
内的白名单文件。
2.
Agent 永远不直接修改正式项目
,先在
`builds/`
中
验证。
3.
只有
构建成功的版本才允许进入站点 Git 历史并成为预览版本
。
1.
Agent 只可读取和修改当前站点
草稿 Worktree
内的白名单文件。
2.
Agent 永远不直接修改正式项目
;修改自动保存到
`draft/`
,用户选择“预览草稿”后再构建
验证。
3.
只有
草稿构建成功才允许提交到站点 Git 历史并成为预览版本;失败草稿继续保留
。
4.
测试预览更新不能覆盖生产产物;生产环境只能通过显式发布操作更新。
5.
`metadata/site.json`
由 WebAgent 独占写入,站点代码不能反向引用它。
6.
删除站点、清理缓存等破坏性操作必须由独立管理接口实现,不能由 Agent Patch 触发。
...
...
frontend/src/App.tsx
View file @
f86dd1a6
This diff is collapsed.
Click to expand it.
frontend/src/api.ts
View file @
f86dd1a6
...
...
@@ -2,6 +2,8 @@ import type { ChatResult, CreateSiteInput, GitHistoryItem, PreviewVersionResult,
async
function
request
<
T
>
(
url
:
string
,
options
?:
RequestInit
):
Promise
<
T
>
{
const
headers
=
new
Headers
(
options
?.
headers
);
const
token
=
sessionStorage
.
getItem
(
"webagent-session"
);
if
(
token
)
headers
.
set
(
"authorization"
,
"Bearer "
+
token
);
if
(
options
?.
body
!=
null
&&
!
headers
.
has
(
"content-type"
))
headers
.
set
(
"content-type"
,
"application/json"
);
const
response
=
await
fetch
(
url
,
{
...
options
,
...
...
@@ -13,6 +15,9 @@ async function request<T>(url: string, options?: RequestInit): Promise<T> {
}
export
const
api
=
{
login
:
(
username
:
string
,
password
:
string
)
=>
request
<
{
token
:
string
;
username
:
string
}
>
(
"/api/login"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
username
,
password
})
}),
logout
:
()
=>
request
<
{
success
:
true
}
>
(
"/api/logout"
,
{
method
:
"POST"
}),
session
:
()
=>
request
<
{
username
:
string
}
>
(
"/api/session"
),
health
:
()
=>
request
<
{
status
:
string
;
agentMode
:
"model"
|
"local"
}
>
(
"/api/health"
),
sites
:
()
=>
request
<
SiteInfo
[]
>
(
"/api/sites"
),
site
:
(
siteId
:
string
)
=>
request
<
SiteInfo
>
(
"/api/sites/"
+
siteId
),
...
...
@@ -20,6 +25,7 @@ export const api = {
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"
),
previewVersion
:
(
siteId
:
string
,
commit
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/preview-version"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
commit
})
}),
previewDraft
:
(
siteId
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/preview-draft"
,
{
method
:
"POST"
}),
rebuild
:
(
siteId
:
string
)
=>
request
<
{
previewUrl
:
string
}
>
(
"/api/sites/"
+
siteId
+
"/build"
,
{
method
:
"POST"
}),
publish
:
(
siteId
:
string
)
=>
request
<
PublishResult
>
(
"/api/sites/"
+
siteId
+
"/publish"
,
{
method
:
"POST"
}),
undo
:
(
siteId
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/undo"
,
{
method
:
"POST"
}),
...
...
frontend/src/styles.css
View file @
f86dd1a6
This diff is collapsed.
Click to expand it.
shared/src/index.ts
View file @
f86dd1a6
...
...
@@ -24,6 +24,9 @@ export interface SiteInfo {
currentCommit
:
string
;
previewCommit
?:
string
;
previousCommit
?:
string
;
draftBaseCommit
?:
string
;
draftUpdatedAt
?:
string
;
draftSummary
?:
string
;
publishedCommit
?:
string
;
publishedAt
?:
string
;
publishStatus
:
PublishStatus
;
...
...
@@ -52,8 +55,9 @@ export interface GitHistoryItem {
export
interface
ChatResult
{
summary
:
string
;
commit
:
string
;
previewUrl
:
string
;
draft
:
true
;
baseCommit
:
string
;
updatedAt
:
string
;
changedFiles
:
string
[];
mode
:
"model"
|
"local"
;
}
...
...
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