2026年在 Web 界面直接编辑 DESIGN.md:从思路到实现

在 Web 界面直接编辑 DESIGN.md:从思路到实现在 MonoSpecs 项目管理系统中 DESIGN md 承载着项目的架构设计和技术决策 但传统的编辑方式要求用户必须切换到外部编辑器 这种割裂的流程 怎么说呢 就像在读一首诗的时候突然被打断了 灵感没了 心情也没了 本文分享了我们在 HagiCode 项目中实践的解决方案 在 Web 界面直接编辑 DESIGN md 并支持从线上设计站点导入模板 毕竟 谁不喜欢一气呵成的感觉呢

大家好,我是讯享网,很高兴认识大家。这里提供最前沿的Ai技术和互联网信息。



在 MonoSpecs 项目管理系统中,DESIGN.md 承载着项目的架构设计和技术决策。但传统的编辑方式要求用户必须切换到外部编辑器,这种割裂的流程,怎么说呢,就像在读一首诗的时候突然被打断了——灵感没了,心情也没了。本文分享了我们在 HagiCode 项目中实践的解决方案:在 Web 界面直接编辑 DESIGN.md,并支持从线上设计站点导入模板。毕竟,谁不喜欢一气呵成的感觉呢?

DESIGN.md 作为项目设计文档的核心载体,承载着架构设计、技术决策和实现指导等关键信息。然而,传统的编辑方式要求用户必须切换到外部编辑器(如 VS Code),手动定位物理路径后再进行编辑。这过程说起来也不算复杂,只是反复几次之后,人也就乏了。

具体问题体现在以下几个方面:

  • 流程割裂:用户需要在 Web 管理界面和本地编辑器之间频繁切换,破坏了工作流连贯性——就像听歌的时候突然断网了,节奏全乱了。
  • 复用困难:设计站点已经发布了丰富的设计模板库,但无法直接集成到项目编辑流程中。明明有好东西,就是用不上,这感觉确实有点遗憾。
  • 体验缺失:缺少”预览-选择-导入”的闭环,用户必须手动复制粘贴,增加了出错风险。手动操作的次数多了,出错的机会自然也多了。
  • 协作障碍:设计文档与代码实现的同步维护变得高摩擦,阻碍团队协作效率。团队协作本就不易,何必再添这些阻力呢?

为了解决这些痛点,我们决定在 Web 界面中实现 DESIGN.md 的直接编辑能力,并支持从线上设计站点一键导入模板。这也不算是什么惊天动地的决策,只是想让开发体验更顺畅一点罢了。

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个 AI 驱动的代码助手项目,在开发过程中,我们需要频繁维护项目的设计文档。为了让团队能够更高效地协作,我们探索并实现了这套在线编辑和导入方案。其实也没什么特别的,只是遇到了问题,想办法解决而已。

整体架构

该解决方案采用前后端分离的同源代理架构,主要由以下几个层次构成。这种架构的设计,说起来也不过是”各司其职”四个字罢了:

1. 前端编辑器层

// 核心组件:DesignMdManagementDrawer // 位置:repos/web/src/components/project/DesignMdManagementDrawer.tsx // 功能:承载编辑、保存、版本冲突检测、导入流程

2. 后端服务层

// 核心服务:ProjectAppService.DesignMd // 位置:repos/hagicode-core/src/PCode.Application/ProjectAppService.DesignMd.cs // 功能:路径解析、文件读写、版本管理

3. 同源代理层

// 代理服务:ProjectAppService.DesignMdSiteIndex // 位置:repos/hagicode-core/src/PCode.Application/ProjectAppService.DesignMdSiteIndex.cs // 功能:代理设计站点资源、预览图缓存、安全校验

关键技术决策

决策 1:全局抽屉模式

采用单一全局抽屉而非局部弹层,通过 layoutSlice 管理状态,实现了跨视图(classic/kanban)的一致体验。这种方式确保用户无论在哪个视图中打开编辑器,都能获得统一的交互体验。毕竟,一致的体验能让用户感觉更自在,不会因为换个视图就迷失方向。

决策 2:项目作用域 API

将 DESIGN.md 相关接口挂在 ProjectController 下,复用现有项目权限边界,避免了新增独立控制器的复杂度。这样设计的好处是权限管理更清晰,也符合 RESTful 的资源组织原则。有时候,复用比重新创建更有意义,不是吗?

决策 3:版本冲突检测

基于文件系统 LastWriteTimeUtc 派生 opaque version,实现了轻量级的乐观并发控制。当多个用户同时编辑同一文件时,系统能够检测到冲突并提示用户刷新。这种设计既不阻塞用户的编辑操作,又能保证数据的一致性——就像人际交往中的边界感,既不过分疏离,也不越界。

决策 4:同源代理模式

通过 IHttpClientFactory 代理外部设计站点资源,避免了跨域问题和 SSRF 风险。这种设计既保证了安全性,又简化了前端调用。安全这件事,做再多也不为过,毕竟数据安全就像健康,失去了才后悔就晚了。

1. 直接编辑 DESIGN.md

后端实现

后端主要负责路径解析、文件读写和版本管理。这些工作虽然基础,但必不可少,就像房子的地基一样:

// 路径解析与安全校验 private Task 
  
    
    
      ResolveDesignDocumentDirectoryAsync(string projectPath, string? repositoryPath) 
    

return ValidateSubPathAsync(projectPath, repositoryPath); 

}

// 版本号生成(基于文件系统时间戳和大小) private static string BuildDesignDocumentVersion(string path) {

var fileInfo = new FileInfo(path); fileInfo.Refresh(); return string.Create( CultureInfo.InvariantCulture, $"{fileInfo.LastWriteTimeUtc.Ticks:x}-{fileInfo.Length:x}"); 

}

版本号的设计其实也挺有意思的,我们用文件的最后修改时间和大小来生成一个唯一的版本标识。这样既轻量又可靠,不需要维护额外的版本数据库。简单的东西,往往更有效,不是吗?

前端实现

前端实现了脏状态检测和保存逻辑。这种设计让用户随时知道自己的修改是否已保存,减少”万一丢失了怎么办”的焦虑:

// 脏状态检测与保存逻辑 const [draft, setDraft] = useState(“); const [savedDraft, setSavedDraft] = useState(”); const isDirty = draft !== savedDraft;

const handleSave = useCallback(async () => );

setSavedDraft(draft); // 更新已保存状态 

}, [activeTarget, document, draft]);

这个实现中,我们维护了两个状态:draft 是当前编辑的内容,savedDraft 是已保存的内容。通过比较两者来判断是否有未保存的修改。这种设计虽然简单,但能给人安心感,毕竟谁愿意辛辛苦苦写的东西突然消失呢?

2. 从线上导入设计文件

目录结构
repos/index/ └── src/data/public/design.json # 设计模板索引

repos/awesome-design-md-site/ ├── vendor/awesome-design-md/ # 上游设计模板 │ └── design-md/ │ ├── clickhouse/ │ │ └── DESIGN.md │ ├── linear/ │ │ └── DESIGN.md │ └── … └── src/lib/content/

└── awesomeDesignCatalog.ts # 内容管线
索引数据格式

设计站点的索引文件定义了所有可用的模板。有了这个索引,用户就能像在餐厅点菜一样,轻松选择自己想要的模板:

{ 

"entries": [

{ "slug": "linear.app", "title": "Linear Inspired Design System", "summary": "AI 产品 / 深色感", "detailUrl": "/designs/linear.app/", "designDownloadUrl": "/designs/linear.app/DESIGN.md", "previewLightImageUrl": "...", "previewDarkImageUrl": "..." } 

] }

每个条目包含了模板的基本信息和下载链接。后端会从这个索引中读取可用的模板列表,然后展示给用户选择。这种设计让选择变得直观,而不是在黑暗中摸索。

同源代理实现

为了保证安全性,后端对设计站点的访问做了严格的校验。安全这件事,再怎么小心也不为过:

// 安全的 slug 校验 private static readonly Regex SafeDesignSiteSlugRegex =

new("^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,127})$", RegexOptions.Compiled); 

private static string NormalizeDesignSiteSlug(string slug)

return normalizedSlug; 

}

// 预览图缓存(OS 临时目录) private static string ComputePreviewCacheKey(string slug, string theme, string previewUrl) {

var raw = $"{slug}|{theme}|{previewUrl}"; var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(raw)); return Convert.ToHexString(bytes).ToLowerInvariant(); 

}

这里我们做了两件事:一是用正则表达式严格校验 slug 的格式,防止路径遍历攻击;二是对预览图进行缓存,减少对外部站点的请求压力。前者是防护,后者是优化,缺一不可罢了。

3. 完整的导入流程

// 1. 打开导入抽屉 const handleRequestImportDrawer = useCallback(() => , []);

// 2. 选择并导入 const handleImportRequest = useCallback((entry) =>

void executeImport(entry); 

}, [isDirty]);

// 3. 执行导入 const executeImport = useCallback(async (entry) => , [activeTarget?.projectId]);

导入流程的设计遵循了”用户确认”的原则:导入后只更新编辑器内容,不会自动保存。用户可以检查导入的内容,确认无误后再手动保存。毕竟,用户对自己写的东西应该有最终决定权,不是吗?

场景 1:项目根目录 DESIGN.md 创建

当 DESIGN.md 不存在时,后端返回虚拟文档状态。这种设计让前端不需要特殊处理”文件不存在”的情况,统一的 API 接口简化了代码逻辑:

return new ProjectDesignDocumentDto ;

// 首次保存时自动创建文件 public async Task SaveDesignDocumentAsync(…) ; }

这种设计的好处是前端不需要特殊处理”文件不存在”的情况,统一的 API 接口简化了代码逻辑。有时候,把复杂性隐藏在后端,前端就能更轻松地专注于用户体验。

场景 2:从设计站点导入模板

用户在导入抽屉中选择 “Linear” 设计模板后,系统会通过后端代理获取 DESIGN.md 内容。整个流程对用户来说是透明的,他们只需要选择模板,系统会自动处理所有的网络请求和数据转换:

// 1. 系统通过后端代理获取 DESIGN.md 内容 GET /api/project/{id}/design-md/site-index/linear.app

// 2. 后端验证 slug 并从上游获取内容 var entry = FindDesignSiteEntry(catalog, "linear.app"); using var upstreamResponse = await httpClient.SendAsync(request); var content = await upstreamResponse.Content.ReadAsStringAsync();

// 3. 前端替换编辑器文本 setDraft(result.content); // 用户检查后手动保存到磁盘

整个流程对用户来说是透明的,他们只需要选择模板,系统会自动处理所有的网络请求和数据转换。用户不需要关心背后的复杂性,这就是我们追求的体验——简单,但强大。

场景 3:版本冲突处理

当多个用户同时编辑同一 DESIGN.md 时,系统会检测到版本冲突。这种乐观并发控制机制确保了数据的一致性,同时又不会阻塞用户的编辑操作:

if (!string.Equals(currentVersion, expectedVersion, StringComparison.Ordinal)) ‘ changed on disk."); }

前端会捕获这个错误并提示用户:

// 前端提示用户刷新并重试 
  
    
    

 
  
    
    
      版本冲突 
     
  
    
    
      文件已被其他进程修改。请刷新最新版本后重试。 
     

这种乐观并发控制机制确保了数据的一致性,同时又不会阻塞用户的编辑操作。冲突不可避免,但至少可以让用户知道发生了什么,而不是默默丢失修改。

1. 路径安全

始终校验 repositoryPath,防止路径遍历攻击。安全这种事,做再多也不为过:

// 始终校验 repositoryPath,防止路径遍历攻击 return ValidateSubPathAsync(projectPath, repositoryPath); // 拒绝 "../", 绝对路径等危险输入

2. 缓存策略

预览图缓存 24 小时,最大 160 个文件。适度的缓存能提升性能,但也不能过度,毕竟平衡才是关键:

// 预览图缓存 24 小时,最大 160 个文件 private static readonly TimeSpan PreviewCacheTtl = TimeSpan.FromHours(24); private const int PreviewCacheMaxFiles = 160; // 定期清理过期缓存

3. 错误处理

上游站点不可用时降级处理。这种优雅降级的设计确保了即使外部依赖不可用,核心编辑功能仍然正常工作。毕竟,不能因为一个外部服务挂了,整个系统就瘫痪了:

// 上游站点不可用时降级处理 try catch (error) {

toast.error(t('project.designMd.siteImport.feedback.catalogLoadFailed')); // 主编辑抽屉仍然可用 

}

这种优雅降级的设计确保了即使外部依赖不可用,核心编辑功能仍然正常工作。系统应该有韧性,而不是一遇到问题就倒下。

4. 用户体验优化

导入前确认覆盖,导入后不自动保存。用户应该对自己的操作有控制权,而不是系统自作主张:

// 导入前确认覆盖 if (isDirty)

// 导入后不自动保存,由用户确认 setDraft(result.content); // 只更新草稿 // 用户检查后点击 Save 才真正写入磁盘

5. 性能考虑

使用 HTTP 客户端工厂,避免创建过多连接。资源管理这种事,看似不起眼,但做好了能带来意想不到的效果:

// 使用 HTTP 客户端工厂,避免创建过多连接 private const string DesignSiteProxyClientName = "ProjectDesignSiteProxy"; private static readonly TimeSpan DesignSiteProxyTimeout = TimeSpan.FromSeconds(8);
  1. Markdown 增强:当前使用基础 Textarea,可考虑升级为 CodeMirror 以支持语法高亮和快捷键。编辑器的体验好了,写文档的心情也会好一些。
  2. 预览模式:添加 Markdown 实时预览,提升编辑体验。所见即所得,总能给人更多信心。
  3. 差异合并:实现智能合并算法,而非简单的全文替换。冲突是难免的,但至少可以让处理冲突的过程不那么痛苦。
  4. 本地缓存:将 design.json 缓存到数据库,减少对外部站点的依赖。依赖越少,系统越稳定,这是简单的道理。

在 HagiCode 项目中,我们通过前后端协作实现了一套完整的 DESIGN.md 在线编辑和导入方案。这套方案的核心价值在于:

  • 提升效率:无需切换工具,在统一的 Web 界面完成设计文档的编辑和导入。省下的时间,可以做更有意义的事情。
  • 降低门槛:设计模板一键导入,新项目可以快速起步。开始得越容易,坚持下去的可能性就越大。
  • 安全可靠:路径校验、版本冲突检测、优雅降级等机制确保系统稳定运行。稳定是基础,没有稳定,一切都是空谈。
  • 用户体验:全局抽屉、脏状态检测、确认对话框等细节打磨了交互体验。细节决定成败,这句话在用户体验上尤其适用。

这套方案已经在 HagiCode 项目中实际运行,解决了团队在设计文档管理方面的痛点。如果你也在面临类似的问题,希望这篇文章能给你一些启发。其实也没什么高深的理论,只是遇到了问题,想办法解决而已。

  • HagiCode 项目地址:github.com/HagiCode-org/site
  • HagiCode 官网:hagicode.com
  • MonoSpecs 项目管理系统:docs.hagicode.com
  • 30 分钟实战演示:www.bilibili.com/video/BV1pirZBuEzq/
  • Docker Compose 安装指南:docs.hagicode.com/installation/docker-compose
  • Desktop 桌面端安装:hagicode.com/desktop/

如果本文对你有帮助,欢迎来 GitHub 给个 Star,公测已开始,现在安装即可参与体验。毕竟,开源项目最缺的就是反馈和鼓励,如果你觉得有用,不妨让它被更多人看到……


“美的事物或人,不一定要占有,只要她还是美的,自己好好看着她的美就好了。”

DESIGN.md 编辑器也是一样,不一定要多么复杂,只要能帮你高效地完成工作,那就是好的罢了。

小讯
上一篇 2026-04-12 23:00
下一篇 2026-04-12 22:58

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/252151.html