七夏 发表于 2025-4-17 14:31:35

完善的轻量说说笔记系统及memos的开源替代品-你的私人朋友圈

<p><img src="https://www.3bbs.cn/index-diy/img.php?url=https://mmbiz.qpic.cn/sz_mmbiz_png/JVqvpxA6UlLiaWTORzgYkKpyYCvOrdJFDtQmW5qZEgniaJ1GJUeTZQ5Dwv1uzw9B7KsERKRdgYk1fwtnoajjDZnw/640?wx_fmt=png&amp;from=appmsg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="T1HaBIgPGZEXlfc" /></p>
<p>前言:近期碰到一个喜欢的一个项目,看到可以类似memos的轻松发布自己的内容...本着喜欢就改造的原则,这个版本的说说笔记就诞生了,使用它,你的数据将由你自由掌控,且可完全免费一键部署,重要的是碎片化的信息记录加自动化就会变的非常高自由度,感兴趣的话不妨来试试</p>
<h2>介绍</h2>
<p>这是基于Ech0基本框架的二次开发、魔改及完善,类似朋友圈样式风格,支持后台配置修改如背景图、个性签名等,支持api获取内容、更新操作等,支持对b站视频、网易云音乐、youtube等的解析添加、支持一键复制,一键生成内容图片、支持httppost发送内容到平台,支持对接webhook、telegram、企业微信、飞书的一键推送,支持内容热力图组件等个性化组件,它完全属于个人的自定化使用,会加入定制化的一些功能,由于代码已重构,不同步于原版</p>
<p>预览:https://note.noisework.cn</p>
<p>源码:https://github.com/rcy1314/echo-noise</p>
<p><img src="https://www.3bbs.cn/index-diy/img.php?url=https://mmbiz.qpic.cn/sz_mmbiz_png/JVqvpxA6UlLiaWTORzgYkKpyYCvOrdJFDuBBTE9HfGsvfhgCsibaibTcoMyaAMKCXznf5V5ZdUYVXuCkruldHzgWw/640?wx_fmt=png&amp;from=appmsg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片" /></p>
<p>原版介绍</p>
<p>Ech0 是一款专为轻量级分享而设计的开源自托管平台,支持快速发布与分享你的想法、文字与链接。简单直观的操作界面,轻松管理你的内容,让分享变得更加自由,确保数据完全掌控,随时随地与世界连接。</p>
<p>原版地址:https://github.com/lin-snow/Ech0</p>
<hr />
<p><img src="https://www.3bbs.cn/index-diy/img.php?url=https://mmbiz.qpic.cn/sz_mmbiz_png/JVqvpxA6UlLiaWTORzgYkKpyYCvOrdJFDiapdqHbeib9dzw295tz2AYIhibAfr5ltFL8hza2iaggQiawVDEnlfRmNTPA/640?wx_fmt=png&amp;from=appmsg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="268shot99" /></p>
<h2>整体改版</h2>
<h3>编辑器部分:</h3>
<ol>
<li>自适应高度和拖拽调整功能</li>
<li>扩展的工具栏功能</li>
<li>完整的响应式支持</li>
<li>平滑的过渡动画效果</li>
<li>优化的间距和字体设置</li>
<li>md格式图片即时预览</li>
<li>添加定制化的组件</li>
</ol>
<h2>主页部分:</h2>
<ol>
<li>调整页面内容自适应高度和宽度</li>
<li>添加随机背景图的展示并带有模糊效果</li>
<li>增加md 格式下对网易云音乐、哔哩哔哩视频、youtube、qq 音乐的解析渲染</li>
<li>调整信息条目的背景ui 及显示尺寸的优化</li>
<li>调整ui及加载响应页面的整体显示效果</li>
<li>添加朋友圈样式主图banner,并和背景图使用相同</li>
<li>所有链接都可通过新标签页打开</li>
<li>长内容的折叠展开处理</li>
<li>完善的二次编辑及预览保存</li>
<li>一键复制及生成内容图片的功能化组件</li>
<li>增加标签系统路由及组件</li>
</ol>
<h2>代码部分</h2>
<ol>
<li>调整jwk验证为session方式,同时调整token的验证机制</li>
<li>调整优化数据库的迁移及连接处理</li>
<li>增加不同的路由及调整控制器</li>
<li>增加额外的外挂插件文件</li>
<li>增加定期清理缓存</li>
</ol>
<h2>特征</h2>
<ul>
<li>
<p>一键部署无服务器平台-fly.io、zeabur、railway、vercel</p>
</li>
<li>
<p>外部扩展-支持快捷指令及popclip一键发布内容到站点</p>
</li>
<li>
<p>支持推送渠道(webhook、tg、企业微信、飞书)</p>
</li>
<li>
<p>标签系统和图片api 路由</p>
</li>
<li>
<p>支持链接远程数据库PostgreSQL、MySQL的连接支持,默认SQLite</p>
</li>
<li>
<p>个性化前端组件如发布日历-热力图组件,默认不显示,点击日历图标后显示<br />
<img src="https://www.3bbs.cn/index-diy/img.php?url=https://mmbiz.qpic.cn/sz_mmbiz_png/JVqvpxA6UlLiaWTORzgYkKpyYCvOrdJFDSsrCzFY1I5X1P7H8s0HK0AKnOQicZliaCCs7fkto8iayE8sS0Bwlm8zQA/640?wx_fmt=png&amp;from=appmsg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="1743765992985_副本" /></p>
<p><img src="https://www.3bbs.cn/index-diy/img.php?url=https://mmbiz.qpic.cn/sz_mmbiz_png/JVqvpxA6UlLiaWTORzgYkKpyYCvOrdJFDOp6DIY8T0ONicUeaiauWRhgnqA2wpbJXdaS4p1CKpKpbIYQNYI9e3BIA/640?wx_fmt=png&amp;from=appmsg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="01.45.31" /></p>
</li>
<li>
<p>内容二次编辑及一键复制一键生成内容图片</p>
</li>
<li>
<p>数据库文件的一键备份、上传<br />
<img src="https://www.3bbs.cn/index-diy/img.php?url=https://mmbiz.qpic.cn/sz_mmbiz_png/JVqvpxA6UlLiaWTORzgYkKpyYCvOrdJFDsvhLicS05TVmDfias5QEv4XpkqCHsnp4sial9olLxNtV0hA3mEYZCicv8w/640?wx_fmt=png&amp;from=appmsg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="ehS1BxwbUKyD2Vm" /></p>
</li>
</ul>
<hr />
<h2>安装部署</h2>
<blockquote>
<p>💡 部署完成后访问 ip:1314 即可使用</p>
</blockquote>
<h2>docker部署</h2>
<p>一键部署</p>
<pre><code>docker run -d \
  --name Ech0-Noise \
  --platform linux/amd64 \
  -p 1314:1314 \
  -v /opt/data/noise.db:/app/data/noise.db \
  noise233/echo-noise
</code></pre>
<p><code>/opt/data/noise.db</code>是你本地的原有数据库文件,如果没有,可以去掉这个挂载命令,它也会自动创建</p>
<p>说明:如果你是经常使用附件图片发布内容的则可以这样:-v /opt/data:/app/data \</p>
<p>默认用户名:admin</p>
<p>默认用户密码:admin</p>
<h3>docker-componse构建部署</h3>
<p>在该目录下执行以下命令启动服务(不修改环境变量时默认使用本地数据库.db 文件):</p>
<pre><code>docker-compose up -d
</code></pre>
<h2>无服务器平台+postgres免费数据库部署</h2>
<p>数据库使用 Neon PostgreSQL 云数据库服务,其它也支持</p>
<p>请先前往官网https://console.neon.tech部署好你的基础数据库</p>
<p>以下部署文件已放入根目录下的noise文件夹内</p>
<p>部署成功示例:</p>
<p><img src="https://www.3bbs.cn/index-diy/img.php?url=https://mmbiz.qpic.cn/sz_mmbiz_png/JVqvpxA6UlLiaWTORzgYkKpyYCvOrdJFDkRaMicetydPCeFnaibibu5bccKKKxhe7AdfYUPxPa9ibPnYWcPBSR4QeQQ/640?wx_fmt=png&amp;from=appmsg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="SDOAt8BsdIiCzXF" /></p>
<h3>Fly.io部署</h3>
<p>fly.toml</p>
<pre><code>app = 'ech0-noise'    # 修改为你的自定义容器名
primary_region = 'hkg'


  auto_rollback = true


  image = 'noise233/echo-noise'
  dockerfile = 'Dockerfile'


  CGO_ENABLED = '1'
  DB_HOST = 'example.aws.neon.tech' # 修改为数据库的HOST地址
  DB_NAME = 'noise'        # 修改为数据库的名称
  DB_PASSWORD = 'example'  # 修改为数据库的密码
  DB_PORT = '5432'
  DB_SSL_MODE = 'require'
  DB_TYPE = 'postgres'
  DB_USER = 'noise_owner'  # 修改为数据库的用户名
  TZ = 'Asia/Shanghai'


  internal_port = 1314
  force_https = true
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 0

[]
  protocol = 'tcp'
  internal_port = 1314

  []
    port = 1314

[]
  memory = '512mb'
  cpu_kind = 'shared'
  cpus = 1
</code></pre>
<p>部署命令 在准备好 fly.toml 文件后,你可以使用以下命令来部署你的应用到 Fly.io:</p>
<h4>初始化 Fly.io 应用(如果尚未初始化)</h4>
<p><code>fly launch</code></p>
<h4>部署应用</h4>
<p><code>fly deploy</code></p>
<p>确保你已经安装并配置好了 Fly.io 的 CLI 工具,并且已经登录到你的 Fly.io 账号。如果你还没有安装 Fly.io CLI,可以通过以下命令安装:</p>
<pre><code>curl -L https://fly.io/install.sh | sh
</code></pre>
<p>安装完成后,使用 <code>fly auth login</code> 登录到你的 Fly.io 账号。</p>
<h3>zeabur部署</h3>
<p>zeabur.toml</p>
<pre><code>app = &quot;ech0-noise&quot;


  dockerfile = &quot;Dockerfile&quot;
  image = &quot;noise233/echo-noise&quot;


  DB_TYPE = &quot;postgres&quot;
  DB_HOST = 'example.aws.neon.tech' # 修改为数据库的HOST地址
  DB_PORT = &quot;5432&quot;
  DB_USER = 'noise_owner'  # 修改为数据库的用户名
  DB_PASSWORD = 'example'  # 修改为数据库的密码
  DB_NAME = 'noise'        # 修改为数据库的名称
  DB_SSL_MODE = &quot;require&quot;
  CGO_ENABLED = &quot;1&quot;
  TZ = &quot;Asia/Shanghai&quot;


  internal_port = 1314
  force_https = true

[]
  protocol = &quot;tcp&quot;
  internal_port = 1314

  []
    port = 1314

[]
  memory = &quot;512mb&quot;
  cpu_kind = &quot;shared&quot;
  cpus = 1
</code></pre>
<h4>部署命令:</h4>
<pre><code>zeabur deploy
</code></pre>
<h3>Railway部署</h3>
<p>railway.toml</p>
<pre><code>app = &quot;ech0-noise&quot;


  dockerfile = &quot;Dockerfile&quot;
  image = &quot;noise233/echo-noise&quot;


  DB_TYPE = &quot;postgres&quot;
  DB_HOST = 'example.aws.neon.tech' # 修改为数据库的HOST地址
  DB_PORT = &quot;5432&quot;
  DB_USER = 'noise_owner'  # 修改为数据库的用户名
  DB_PASSWORD = 'example'  # 修改为数据库的密码
  DB_NAME = 'noise'        # 修改为数据库的名称
  DB_SSL_MODE = &quot;require&quot;
  CGO_ENABLED = &quot;1&quot;
  TZ = &quot;Asia/Shanghai&quot;


  internal_port = 1314
  protocol = &quot;tcp&quot;


  port = 1314


  memory = &quot;512mb&quot;
  cpu_kind = &quot;shared&quot;
  cpus = 1
</code></pre>
<h4>部署命令:</h4>
<pre><code>railway up
</code></pre>
<p><img src="https://www.3bbs.cn/index-diy/img.php?url=https://mmbiz.qpic.cn/sz_mmbiz_png/JVqvpxA6UlLiaWTORzgYkKpyYCvOrdJFDVf7iaSGmoDnARujsfQ10Is3ib8OPibBL0dm3ibZliaibHgwYnq77Qa4yb0dA/640?wx_fmt=png&amp;from=appmsg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="预览" /></p>
<p>注意⚠️</p>
<p>如果你是直接在平台拉取项目部署而不是通过命令部署,你需要拷贝fork本项目并将fly.toml、railway.toml、zeabur.toml文件放入根目录下才能一键部署</p>
<h2>数据库连接部分</h2>
<p>本地数据库直接docker部署即可</p>
<p>远程数据库服务则可以通过环境变量连接</p>
<p>连接远程 PostgreSQL:</p>
<pre><code>docker run -d \
  --name Ech0-Noise \
  --platform linux/amd64 \
  -p 1314:1314 \
  -e DB_TYPE=postgres \
  -e DB_HOST=your.postgres.host \
  -e DB_PORT=5432 \
  -e DB_USER=your_username \
  -e DB_PASSWORD=your_password \
  -e DB_NAME=noise \
  -v /opt/data/images:/app/data/images \
  noise233/echo-noise
</code></pre>
<p>连接远程 MySQL:</p>
<pre><code>docker run -d \
  --name Ech0-Noise \
  --platform linux/amd64 \
  -p 1314:1314 \
  -e DB_TYPE=mysql \
  -e DB_HOST=your.mysql.host \
  -e DB_PORT=3306 \
  -e DB_USER=your_username \
  -e DB_PASSWORD=your_password \
  -e DB_NAME=noise \
  -v /opt/data/images:/app/data/images \
  noise233/echo-noise
</code></pre>
<p>注意事项:</p>
<ol>
<li>确保远程数据库允许外部连接</li>
<li>检查防火墙设置</li>
<li>使用正确的数据库连接信息</li>
<li>建议使用加密连接</li>
<li>注意数据库的字符集设置</li>
</ol>
<p>对于 Neon PostgreSQL (地址https://console.neon.tech )这样的云数据库服务,需要使用特定的连接参数。以下是连接命令:</p>
<pre><code>docker run -d \
  --name Ech0-Noise \
  --platform linux/amd64 \
  -p 1314:1314 \
  -e DB_TYPE=postgres \
  -e DB_HOST=your.host \
  -e DB_PORT=5432 \
  -e DB_USER=user_owner \
  -e DB_PASSWORD=password \
  -e DB_NAME=yourname \
  -e DB_SSL_MODE=require \
  -v /opt/data/images:/app/data/images \
  noise233/echo-noise
</code></pre>
<p>注意事项:</p>
<ol>
<li>添加了 <code>DB_SSL_MODE=require</code> 环境变量,因为 Neon 要求 SSL 连接</li>
<li>使用了连接 URL 中提供的主机名、用户名、密码和数据库名</li>
<li>保持图片目录的挂载</li>
</ol>
<h2>数据的备份恢复</h2>
<p>对于所有数据库类型(SQLite/PostgreSQL/MySQL),点击后台数据库下载按钮后,都会先备份数据库文件</p>
<ul>
<li>
<p>然后会将包含数据库备份和图片打包成 zip 文件</p>
</li>
<li>
<p>zip 文件中会包含:</p>
<ul>
<li>数据库备份文件(.db/.sql)</li>
<li>images 目录下的所有图片</li>
</ul>
</li>
</ul>
<pre><code>备份过程:
本地 -&gt; 执行备份命令 -&gt; 生成备份文件 -&gt; 打包下载

恢复过程:
上传备份文件 -&gt; 解压缩 -&gt; 执行恢复命令 -&gt; 导入到云数据库
</code></pre>
<p>恢复要求:</p>
<ul>
<li>SQLite本地数据库备份和上传时默认使用的文件名是一致为noise.db</li>
<li>非本地数据库PostgreSQL/MySQL请命名为database.sql并放入database.zip来恢复</li>
<li>如果备份时zip中有图片文件夹则同时会恢复 images 目录下的所有图片</li>
</ul>
<p>⚠️ :因PostgreSQL/MySQL云服务会有SSL连接、兼容版本号、数据表格式等要求,后台一键备份恢复不一定能满足你需要连接的远程数据库,请尽量前往服务商处下载备份</p>
<h2>API指南🧭</h2>
<p>先到后台获取api token,然后可以参考下面的命令运行或使用其它服务(记得将https://your.localhost.com 更改为你自己的服务地址)</p>
<p><img src="https://www.3bbs.cn/index-diy/img.php?url=https://mmbiz.qpic.cn/sz_mmbiz_png/JVqvpxA6UlLiaWTORzgYkKpyYCvOrdJFDEWEicX55Uk3qdE4ocSLjwdoGYErEqsyIPHlibK8yN8eCx8Npz8Q32lQA/640?wx_fmt=png&amp;from=appmsg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="1743847126537" /></p>
<pre><code># 发送纯文本信息
curl -X POST 'https://your.localhost.com/api/token/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer c721249bd66e1133fba430ea9e3c32f1' \
-d '{
  &quot;content&quot;: &quot;测试信息&quot;,
  &quot;type&quot;: &quot;text&quot;
}'
</code></pre>
<pre><code># 方式1:使用 Markdown 语法发送文本
curl -X POST 'https://your.localhost.com/api/token/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: c721249bd66e1133fba430ea9e3c32f1' \
-d '{
  &quot;content&quot;: &quot;# 标题\n这是一段文字\n![图片描述](https://example.com/image.jpg)&quot;,
  &quot;type&quot;: &quot;text&quot;
}'

# 方式2:使用 type: image 发送图片消息
curl -X POST 'https://your.localhost.com/api/token/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: c721249bd66e1133fba430ea9e3c32f1' \
-d '{
  &quot;content&quot;: &quot;图片描述文字&quot;,
  &quot;type&quot;: &quot;image&quot;,
  &quot;image&quot;: &quot;https://example.com/image.jpg&quot;
}'
</code></pre>
<p>如果你想使用session 认证方式</p>
<pre><code>curl -v -X POST 'https://your.localhost.com/api/messages' \
-H 'Content-Type: application/json' \
--cookie &quot;your_session_cookie&quot; \
-d '{
  &quot;content&quot;: &quot;测试信息&quot;,
  &quot;type&quot;: &quot;text&quot;
}'
</code></pre>
<p>对于图文混合消息,可以这样发送:</p>
<pre><code>curl -X POST 'https://your.localhost.com/api/token/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: c721249bd66e1133fba430ea9e3c32f1' \
-d '{
  &quot;content&quot;: &quot;# 这是标题\n\n这是一段文字说明\n\n![图片描述](https://example.com/image.jpg)\n\n继续写文字内容&quot;,
  &quot;type&quot;: &quot;text&quot;
}'
</code></pre>
<pre><code>或者使用 multipart 类型:

curl -X POST 'https://your.localhost.com/api/token/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: c721249bd66e1133fba430ea9e3c32f1' \
-d '{
  &quot;content&quot;: &quot;# 这是标题\n\n这是一段文字说明&quot;,
  &quot;type&quot;: &quot;multipart&quot;,
  &quot;image&quot;: &quot;https://example.com/image.jpg&quot;
}
</code></pre>
<h1>API 文档(待增加)</h1>
<h2>公共接口</h2>
<h3>1. 获取前端配置</h3>
<ul>
<li><strong>路径</strong>: <code>/api/frontend/config</code></li>
<li><strong>方法</strong>: GET</li>
<li><strong>描述</strong>: 获取前端配置信息</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl http://localhost:8080/api/frontend/config
</code></pre>
<h3>2. 用户登录</h3>
<ul>
<li><strong>路径</strong>: <code>/api/login</code></li>
<li><strong>方法</strong>: POST</li>
<li><strong>描述</strong>: 用户登录接口</li>
<li><strong>请求体</strong>:</li>
</ul>
<pre><code>{
    &quot;username&quot;: &quot;admin&quot;,
    &quot;password&quot;: &quot;password&quot;
}
</code></pre>
<ul>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X POST http://localhost:8080/api/login \
     -H &quot;Content-Type: application/json&quot; \
     -d '{&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;password&quot;}'
</code></pre>
<h3>3. 用户注册</h3>
<ul>
<li><strong>路径</strong>: <code>/api/register</code></li>
<li><strong>方法</strong>: POST</li>
<li><strong>描述</strong>: 用户注册接口</li>
<li><strong>请求体</strong>:</li>
</ul>
<pre><code>{
    &quot;username&quot;: &quot;newuser&quot;,
    &quot;password&quot;: &quot;password&quot;,
    &quot;email&quot;: &quot;user@example.com&quot;
}
</code></pre>
<ul>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X POST http://localhost:8080/api/register \
     -H &quot;Content-Type: application/json&quot; \
     -d '{&quot;username&quot;:&quot;newuser&quot;,&quot;password&quot;:&quot;password&quot;,&quot;email&quot;:&quot;user@example.com&quot;}'
</code></pre>
<h3>4. 获取系统状态</h3>
<ul>
<li><strong>路径</strong>: <code>/api/status</code></li>
<li><strong>方法</strong>: GET</li>
<li><strong>描述</strong>: 获取系统运行状态</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl http://localhost:8080/api/status
</code></pre>
<h3>5. 消息相关公共接口</h3>
<h4>5.1 获取所有消息</h4>
<ul>
<li><strong>路径</strong>: <code>/api/messages</code></li>
<li><strong>方法</strong>: GET</li>
<li><strong>描述</strong>: 获取所有公开消息</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl http://localhost:8080/api/messages
</code></pre>
<h4>5.2 获取单条消息</h4>
<ul>
<li><strong>路径</strong>: <code>/api/messages/:id</code></li>
<li><strong>方法</strong>: GET</li>
<li><strong>描述</strong>: 获取指定ID的消息</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl http://localhost:8080/api/messages/1
</code></pre>
<h4>5.3 分页获取消息</h4>
<ul>
<li><strong>路径</strong>: <code>/api/messages/page</code></li>
<li><strong>方法</strong>: POST或GET</li>
<li><strong>描述</strong>: 分页获取消息列表</li>
<li><strong>请求体</strong>:</li>
</ul>
<pre><code>{
    &quot;page&quot;: 1,
    &quot;pageSize&quot;: 10
}
</code></pre>
<ul>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X POST http://localhost:8080/api/messages/page \
     -H &quot;Content-Type: application/json&quot; \
     -d '{&quot;page&quot;:1,&quot;pageSize&quot;:10}'
</code></pre>
<h4>5.4 获取消息日历数据</h4>
<ul>
<li><strong>路径</strong>: <code>/api/messages/calendar</code></li>
<li><strong>方法</strong>: GET</li>
<li><strong>描述</strong>: 获取消息发布热力图数据</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl http://localhost:8080/api/messages/calendar
</code></pre>
<h4>5.5 搜索消息</h4>
<ul>
<li>
<p><strong>路径</strong>: <code>/api/messages/search</code></p>
</li>
<li>
<p><strong>方法</strong>: GET</p>
</li>
<li>
<p><strong>参数</strong>:</p>
<ul>
<li>keyword: 搜索关键词</li>
<li>page: 页码</li>
<li>pageSize: 每页数量</li>
</ul>
</li>
<li>
<p><strong>示例请求</strong>:</p>
</li>
</ul>
<pre><code>curl &quot;http://localhost:8080/api/messages/search?keyword=测试&amp;page=1&amp;pageSize=10&quot;
</code></pre>
<h3>6. RSS 相关接口</h3>
<h4>6.1 获取 RSS 订阅</h4>
<ul>
<li><strong>路径</strong>: <code>/rss</code></li>
<li><strong>方法</strong>: GET</li>
<li><strong>描述</strong>: 获取 RSS 订阅内容</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl http://localhost:1314/rss
</code></pre>
<h2>需要认证的接口</h2>
<h3>1. 消息操作接口</h3>
<h4>1.1 发布消息</h4>
<ul>
<li><strong>路径</strong>: <code>/api/messages</code></li>
<li><strong>方法</strong>: POST</li>
<li><strong>描述</strong>: 发布新消息</li>
<li><strong>请求体</strong>:</li>
</ul>
<pre><code>{
    &quot;content&quot;: &quot;消息内容&quot;,
    &quot;private&quot;: false,
    &quot;imageURL&quot;: &quot;&quot;
}
</code></pre>
<ul>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X POST http://localhost:8080/api/messages \
     -H &quot;Content-Type: application/json&quot; \
     -H &quot;Cookie: session=xxx&quot; \
     -d '{&quot;content&quot;:&quot;测试消息&quot;,&quot;private&quot;:false}'
</code></pre>
<h4>1.2 更新消息</h4>
<ul>
<li><strong>路径</strong>: <code>/api/messages/:id</code></li>
<li><strong>方法</strong>: PUT</li>
<li><strong>描述</strong>: 更新指定消息</li>
<li><strong>请求体</strong>:</li>
</ul>
<pre><code>{
    &quot;content&quot;: &quot;更新后的内容&quot;
}
</code></pre>
<ul>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X PUT http://localhost:8080/api/messages/1 \
     -H &quot;Content-Type: application/json&quot; \
     -H &quot;Cookie: session=xxx&quot; \
     -d '{&quot;content&quot;:&quot;更新后的内容&quot;}'
</code></pre>
<h4>1.3 删除消息</h4>
<ul>
<li><strong>路径</strong>: <code>/api/messages/:id</code></li>
<li><strong>方法</strong>: DELETE</li>
<li><strong>描述</strong>: 删除指定消息</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X DELETE http://localhost:8080/api/messages/1 \
     -H &quot;Cookie: session=xxx&quot;
</code></pre>
<h3>2. 用户相关接口</h3>
<h4>2.1 获取用户信息</h4>
<ul>
<li><strong>路径</strong>: <code>/api/user</code></li>
<li><strong>方法</strong>: GET</li>
<li><strong>描述</strong>: 获取当前登录用户信息</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl http://localhost:8080/api/user \
     -H &quot;Cookie: session=xxx&quot;
</code></pre>
<h4>2.2 修改密码</h4>
<ul>
<li><strong>路径</strong>: <code>/api/user/change_password</code></li>
<li><strong>方法</strong>: PUT</li>
<li><strong>请求体</strong>:</li>
</ul>
<pre><code>{
    &quot;oldPassword&quot;: &quot;旧密码&quot;,
    &quot;newPassword&quot;: &quot;新密码&quot;
}
</code></pre>
<ul>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X PUT http://localhost:8080/api/user/change_password \
     -H &quot;Content-Type: application/json&quot; \
     -H &quot;Cookie: session=xxx&quot; \
     -d '{&quot;oldPassword&quot;:&quot;old&quot;,&quot;newPassword&quot;:&quot;new&quot;}'
</code></pre>
<h4>2.3 更新用户信息</h4>
<ul>
<li><strong>路径</strong>: <code>/api/user/update</code></li>
<li><strong>方法</strong>: PUT</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X PUT http://localhost:8080/api/user/update \
     -H &quot;Content-Type: application/json&quot; \
     -H &quot;Cookie: session=xxx&quot; \
     -d '{&quot;username&quot;:&quot;newname&quot;}'
</code></pre>
<h4>2.4 退出登录</h4>
<ul>
<li><strong>路径</strong>: <code>/api/user/logout</code></li>
<li><strong>方法</strong>: POST</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X POST http://localhost:8080/api/user/logout \
     -H &quot;Cookie: session=xxx&quot;
</code></pre>
<h3>3. Token 相关接口</h3>
<h4>3.1 获取用户 Token</h4>
<ul>
<li><strong>路径</strong>: <code>/api/user/token</code></li>
<li><strong>方法</strong>: GET</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl http://localhost:8080/api/user/token \
     -H &quot;Cookie: session=xxx&quot;
</code></pre>
<h4>3.2 重新生成 Token</h4>
<ul>
<li><strong>路径</strong>: <code>/api/user/token/regenerate</code></li>
<li><strong>方法</strong>: POST</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X POST http://localhost:8080/api/user/token/regenerate \
     -H &quot;Cookie: session=xxx&quot;
</code></pre>
<h3>4. 系统设置接口</h3>
<h4>4.1 更新系统设置</h4>
<ul>
<li><strong>路径</strong>: <code>/api/settings</code></li>
<li><strong>方法</strong>: PUT</li>
<li><strong>请求体</strong>:</li>
</ul>
<pre><code>{
    &quot;allowRegistration&quot;:true,
&quot;frontendSettings&quot;:{
&quot;siteTitle&quot;:&quot;网站标题&quot;,
&quot;subtitleText&quot;:&quot;副标题&quot;,
&quot;avatarURL&quot;:&quot;头像URL&quot;,
&quot;username&quot;:&quot;显示用户名&quot;,
&quot;description&quot;:&quot;描述&quot;,
&quot;backgrounds&quot;:[&quot;背景图URL&quot;],
&quot;cardFooterTitle&quot;:&quot;页脚标题&quot;,
&quot;cardFooterLink&quot;:&quot;页脚链接&quot;,
&quot;pageFooterHTML&quot;:&quot;页脚HTML&quot;,
&quot;rssTitle&quot;:&quot;RSS标题&quot;,
&quot;rssDescription&quot;:&quot;RSS描述&quot;,
&quot;rssAuthorName&quot;:&quot;RSS作者&quot;,
&quot;rssFaviconURL&quot;:&quot;RSS图标URL&quot;,
&quot;walineServerURL&quot;:&quot;评论系统URL&quot;
}
}
</code></pre>
<ul>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X PUT http://localhost:8080/api/settings \
     -H &quot;Content-Type: application/json&quot; \
     -H &quot;Cookie: session=xxx&quot; \
     -d '{&quot;allowRegistration&quot;:true,&quot;frontendSettings&quot;:{&quot;siteTitle&quot;:&quot;我的网站&quot;}}'
</code></pre>
<h3>5. 备份相关接口</h3>
<h4>5.1 下载备份</h4>
<ul>
<li><strong>路径</strong>: <code>/api/backup/download</code></li>
<li><strong>方法</strong>: GET</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl http://localhost:8080/api/backup/download \
     -H &quot;Cookie: session=xxx&quot; \
     --output backup.sql
</code></pre>
<h4>5.2 恢复备份</h4>
<ul>
<li><strong>路径</strong>: <code>/api/backup/restore</code></li>
<li><strong>方法</strong>: POST</li>
<li><strong>描述</strong>: 从备份文件恢复数据</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X POST http://localhost:8080/api/backup/restore \
     -H &quot;Cookie: session=xxx&quot; \
     -F &quot;file=@backup.sql&quot;
</code></pre>
<h3>6. 图片上传接口</h3>
<h4>6.1 上传图片</h4>
<ul>
<li><strong>路径</strong>: <code>/api/images/upload</code></li>
<li><strong>方法</strong>: POST</li>
<li><strong>描述</strong>: 上传图片文件</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X POST http://localhost:8080/api/images/upload \
     -H &quot;Cookie: session=xxx&quot; \
     -F &quot;file=@image.jpg&quot;
</code></pre>
<h3>7.推送配置路由使用说明</h3>
<h4>获取推送配置</h4>
<ul>
<li><strong>路径</strong>: <code>/api/notify/config</code></li>
<li><strong>方法</strong>: GET</li>
<li><strong>描述</strong>: 获取当前推送渠道配置</li>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X GET http://localhost:8080/api/notify/config \
     -H &quot;Cookie: session=xxx&quot;
</code></pre>
<h4>保存推送配置</h4>
<ul>
<li><strong>路径</strong>: <code>/api/notify/config</code></li>
<li><strong>方法</strong>: PUT</li>
<li><strong>描述</strong>: 更新推送渠道配置</li>
<li><strong>请求体示例</strong>:</li>
</ul>
<pre><code>{
  &quot;webhookEnabled&quot;:true,
&quot;webhookURL&quot;:&quot;https://webhook.example.com&quot;,
&quot;telegramEnabled&quot;:true,
&quot;telegramToken&quot;:&quot;bot123:ABC&quot;,
&quot;telegramChatID&quot;:&quot;-100123456&quot;,
&quot;weworkEnabled&quot;:false,
&quot;weworkKey&quot;:&quot;&quot;,
&quot;feishuEnabled&quot;:true,
&quot;feishuWebhook&quot;:&quot;https://open.feishu.cn/xxx&quot;,
&quot;feishuSecret&quot;:&quot;signature_key&quot;
}
</code></pre>
<ul>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X PUT http://localhost:8080/api/notify/config \
     -H &quot;Cookie: session=xxx&quot; \
     -H &quot;Content-Type: application/json&quot; \
     -d '{
           &quot;webhookEnabled&quot;: true,
           &quot;webhookURL&quot;: &quot;https://webhook.example.com&quot;
         }'
</code></pre>
<h4>测试推送</h4>
<ul>
<li><strong>路径</strong>: <code>/api/notify/test</code></li>
<li><strong>方法</strong>: POST</li>
<li><strong>描述</strong>: 测试指定推送渠道</li>
<li><strong>请求体示例</strong>:</li>
</ul>
<pre><code>{
  &quot;type&quot;: &quot;telegram&quot;
}
</code></pre>
<ul>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X POST http://localhost:8080/api/notify/test \
     -H &quot;Cookie: session=xxx&quot; \
     -H &quot;Content-Type: application/json&quot; \
     -d '{&quot;type&quot;: &quot;telegram&quot;}'
</code></pre>
<h4>发送推送</h4>
<ul>
<li><strong>路径</strong>: <code>/api/notify/send</code></li>
<li><strong>方法</strong>: POST</li>
<li><strong>描述</strong>: 手动触发推送(需已配置推送渠道)</li>
<li><strong>请求体示例</strong>:</li>
</ul>
<pre><code>{
  &quot;content&quot;: &quot;测试消息内容&quot;,
  &quot;images&quot;: [&quot;https://example.com/image.jpg&quot;],
  &quot;format&quot;: &quot;markdown&quot;
}
</code></pre>
<ul>
<li><strong>示例请求</strong>:</li>
</ul>
<pre><code>curl -X POST http://localhost:8080/api/notify/send \
     -H &quot;Cookie: session=xxx&quot; \
     -H &quot;Content-Type: application/json&quot; \
     -d '{&quot;content&quot;: &quot;紧急通知!&quot;}'
</code></pre>
<p>注意事项:</p>
<ol>
<li>所有需要认证的接口都需要在请求头中携带有效的 session cookie</li>
<li>部分接口可能需要管理员权限</li>
<li>所有请求示例中的域名和端口号需要根据实际部署情况调整</li>
<li>文件上传接口需要使用 multipart/form-data 格式</li>
<li>Token 认证接口可以使用 Token 替代 session 进行认证</li>
</ol>
<h2>发布说明</h2>
<p>目前会构建两个版本,</p>
<p>稳定版:latest镜像</p>
<p>实验版:last镜像</p>
<p>如果你需要构建自己的镜像发布-示例:</p>
<pre><code>docker buildx build --platform linux/amd64,linux/arm64 -t noise233/echo-noise:latest --push --no-cache .
</code></pre>
<h1>Memos数据库迁移示例</h1>
<p>其中,你需要设置设置源数据库和目标数据库的路径,源数据库为memos_prod.db(memos数据)目标数据库为database.db(本站数据库),你还需要修改构建插入的数据中的用户名为你自己的用户名,分别迁移了原文本内容、发布时间,可以在noise/memos迁移文件夹中找到该脚本</p>
<p>,运行python3 main.py即可,</p>
<p><img src="https://www.3bbs.cn/index-diy/img.php?url=https://mmbiz.qpic.cn/sz_mmbiz_png/JVqvpxA6UlLiaWTORzgYkKpyYCvOrdJFDmpTRofopb5tXxDC732PfY0EMiaMBrDako6t8ZnaCaicoFf2HZQYjypLQ/640?wx_fmt=png&amp;from=appmsg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="1744202949838" /></p>
<p>迁移结束后将你的数据库文件和原图片文件夹(有的话)打包为zip格式,进入站点后台选择恢复数据上传即可。</p>
<h2>Popclip发送扩展</h2>
<p>选中后自动识别安装,发送时会自动添加一个popclip开头的标签,token可在后台找到</p>
<pre><code>// #popclip extension for Send to Shuo
// name: 说说笔记
// icon: square filled 说
// language: javascript
// module: true
// entitlements:
// options: [{
//   identifier: &quot;siteUrl&quot;,
//   label: &quot;服务端地址&quot;,
//   type: &quot;string&quot;,
//   defaultValue: &quot;https://note.noisework.cn&quot;,
//   description: &quot;请确保地址正确,不要带末尾斜杠&quot;
// }, {
//   identifier: &quot;token&quot;,
//   label: &quot;API Token&quot;,
//   type: &quot;string&quot;,
//   description: &quot;从设置页面获取最新Token&quot;
// }]

async function sendToShuo(input, options) {
    try {
        // 参数预处理
        const siteUrl = (options.siteUrl || &quot;&quot;).replace(/\/+$/g, &quot;&quot;);
        const token = (options.token || &quot;&quot;).trim();
        const content = (input.text || &quot;&quot;).trim();

        // 验证参数
        if (!/^https:\/\/[\w.-]+(:\d+)?$/.test(siteUrl)) {
            throw new Error(&quot;地址格式错误,示例: https://note.noisework.cn&quot;);
        }
        if (!token) throw new Error(&quot;Token不能为空&quot;);
        if (!content) throw new Error(&quot;选中文本不能为空&quot;);

        // 发送请求
        await sendRequestWithXMLHttpRequest(siteUrl, token, content);
        PopClip.showText(&quot;✓ 发送成功&quot;);
    } catch (error) {
        handleRequestError(error);
    }
}

// 使用 XMLHttpRequest 实现网络请求
function sendRequestWithXMLHttpRequest(siteUrl, token, content) {
    return new Promise((resolve, reject) =&gt; {
        const xhr = new XMLHttpRequest();
        const url = `${siteUrl}/api/token/messages`;

        xhr.open(&quot;POST&quot;, url, true);
        xhr.setRequestHeader(&quot;Content-Type&quot;, &quot;application/json&quot;);
        xhr.setRequestHeader(&quot;Authorization&quot;, `Bearer ${token}`);

        xhr.timeout = 10000; // 设置超时时间(10秒)

        // 设置回调函数
        xhr.onreadystatechange = () =&gt; {
            if (xhr.readyState === XMLHttpRequest.DONE) {
                if (xhr.status &gt;= 200 &amp;&amp; xhr.status &lt; 300) {
                    resolve(xhr.responseText);
                } else {
                    let errorMsg = `请求失败 (${xhr.status})`;
                    try {
                        const data = JSON.parse(xhr.responseText);
                        errorMsg = data.message || errorMsg;
                    } catch {}
                    reject(new Error(errorMsg));
                }
            }
        };

        // 处理网络错误
        xhr.onerror = () =&gt; reject(new Error(&quot;网络错误&quot;));

        // 处理超时错误
        xhr.ontimeout = () =&gt; reject(new Error(&quot;请求超时&quot;));

        try {
            // 发送请求
            const payload = JSON.stringify({
                content: `#Popclip\n${content}`,
                type: &quot;text&quot;
            });
            xhr.send(payload);
        } catch (error) {
            reject(new Error(&quot;请求发送失败: &quot; + error.message));
        }
    });
}

// 错误处理
function handleRequestError(error) {
    console.error(&quot;请求错误:&quot;, error);

    const errorMap = {
        &quot;Failed to fetch&quot;: &quot;无法连接到服务器&quot;,
        &quot;aborted&quot;: &quot;请求超时&quot;,
        &quot;网络错误&quot;: &quot;网络错误&quot;,
        &quot;401&quot;: &quot;认证失败,请检查Token&quot;,
        &quot;404&quot;: &quot;API地址不存在&quot;
    };

    const message = Object.entries(errorMap).find(() =&gt; 
        error.message.includes(key)
    )?. || `请求错误: ${error.message.split('\n').slice(0, 50)}`;

    PopClip.showText(`❌ ${message}`);
}

exports.actions = [{
    title: &quot;发送至说说笔记&quot;,
    code: sendToShuo,
    icon: &quot;square filled 说&quot;
}];
</code></pre>
<h2>Web组件示例</h2>
<p>如果你想将内容作为说说嵌入或结合到你的网站、博客可以参考</p>
<p>说明:host为站点地址,limit为每页内容数量,domId为容器名,下面的代码展示了使用js来请求数据内容到前端并渲染处理的基本框架,其余需要你自己再丰富css样式和你自己的页面</p>
<p>html前端:</p>
<pre><code>




        加载中...



    var note = {
        host: 'https://note.noisework.cn', //请修改为你自己的站点地址
        limit: '10',
        domId: '#note'
    }



</code></pre>
<p>note.js</p>
<pre><code>// Note says content loading script
document.addEventListener('DOMContentLoaded', function() {
// get parameters from global configuration
const config = window.note || {
host: 'https://note.noisework.cn',
limit: '10',
domId: '#note'
    };

// 修改容器选择器
const container = document.querySelector('#note .note-container');
let currentPage = 1;
let isLoading = false;
let hasMore = true;

// create load more button
const loadMoreBtn = document.createElement('button');
    loadMoreBtn.id = 'load-more-note';
    loadMoreBtn.className = 'load-more';
    loadMoreBtn.textContent = '加载更多';
    loadMoreBtn.style.display = 'none';
    loadMoreBtn.addEventListener('click', loadMoreContent);

// create already loaded all prompt
const loadedAll = document.createElement('div');
    loadedAll.id = 'loaded-all-note';
    loadedAll.className = 'loaded-all';
    loadedAll.textContent = '已加载全部';
    loadedAll.style.display = 'none';

    container.appendChild(loadMoreBtn);
    container.appendChild(loadedAll);

// initial load
loadInitialContent();

asyncfunctionloadInitialContent() {
try {
console.log(`请求URL: ${config.host}/api/messages/page?page=${currentPage}&amp;pageSize=${config.limit}`);
const response = awaitfetch(`${config.host}/api/messages/page?page=${currentPage}&amp;pageSize=${config.limit}`);

if (!response.ok) {
thrownewError(`HTTP error! status: ${response.status}`);
            }

const result = await response.json();
console.log('API响应数据:', result);

// 修改为检查result.data.items
if (result &amp;&amp; result.code === 1 &amp;&amp; result.data &amp;&amp; result.data.items &amp;&amp; Array.isArray(result.data.items)) {
const sortedData = result.data.items.sort((a, b) =&gt;
newDate(b.created_at) - newDate(a.created_at)
                );
renderMessages(sortedData);

if (result.data.items.length &gt;= config.limit) {
                    loadMoreBtn.style.display = 'block';
                } else {
                    loadedAll.style.display = 'block';
                    hasMore = false;
                }
            } else {
                container.querySelector('.loading-wrapper').textContent = '暂无内容';
                hasMore = false;
            }
        } catch (error) {
console.error('加载内容失败:', error);
            container.querySelector('.loading-wrapper').textContent = '加载失败,请刷新重试';
        }
    }

asyncfunctionloadMoreContent() {
if (isLoading || !hasMore) return;

        isLoading = true;
        loadMoreBtn.textContent = '加载中...';
        currentPage++;

try {
const response = awaitfetch(`${config.host}/api/messages/page?page=${currentPage}&amp;pageSize=${config.limit}`);
if (!response.ok) {
thrownewError(`HTTP error! status: ${response.status}`);
            }
const result = await response.json();

// 同样修改为检查result.data.items
if (result &amp;&amp; result.code === 1 &amp;&amp; result.data &amp;&amp; result.data.items &amp;&amp; Array.isArray(result.data.items)) {
const sortedData = result.data.items.sort((a, b) =&gt;
newDate(b.created_at) - newDate(a.created_at)
                );
renderMessages(sortedData);

if (result.data.items.length &lt; config.limit) {
                    loadMoreBtn.style.display = 'none';
                    loadedAll.style.display = 'block';
                    hasMore = false;
                }
            } else {
                loadMoreBtn.style.display = 'none';
                loadedAll.style.display = 'block';
                hasMore = false;
            }
        } catch (error) {
console.error('加载更多内容失败:', error);
            currentPage--;
        } finally {
            isLoading = false;
            loadMoreBtn.textContent = '加载更多';
        }
    }

functionrenderMessages(messages) {
const loadingWrapper = container.querySelector('.loading-wrapper');
if (loadingWrapper) {
            loadingWrapper.style.display = 'none';
        }

        messages.forEach(message =&gt; {
const messageElement = createMessageElement(message);
            container.insertBefore(messageElement, loadMoreBtn);
        });
    }

functioncreateMessageElement(message) {
const messageDiv = document.createElement('div');
        messageDiv.className = 'rssmergecard';

const contentDiv = document.createElement('div');
        contentDiv.className = 'rssmergecard-content';

const title = document.createElement('h3');
        title.className = 'rssmergecard-title';
        title.textContent = message.username || '匿名用户';

const description = document.createElement('div');
        description.className = 'rssmergecard-description';

// 解析Markdown内容和特殊链接
let processedContent = message.content || '无内容';
        processedContent = parseMarkdown(processedContent);
        processedContent = parseSpecialLinks(processedContent);
        description.innerHTML = processedContent;

// 如果有图片则添加图片
if (message.image_url) {
const img = document.createElement('img');
            img.src = message.image_url.startsWith('http') ? message.image_url : config.host + message.image_url;
            img.style.maxWidth = '100%';
            img.style.borderRadius = '6px';
            img.style.margin = '10px 0';
            description.appendChild(img);
        }

const metaDiv = document.createElement('div');
        metaDiv.className = 'rssmergecard-meta';

const timeSpan = document.createElement('span');
        timeSpan.className = 'rssmergecard-time';
        timeSpan.textContent = formatDate(message.created_at);

        metaDiv.appendChild(timeSpan);
        contentDiv.appendChild(title);
        contentDiv.appendChild(description);
        contentDiv.appendChild(metaDiv);
        messageDiv.appendChild(contentDiv);

return messageDiv;
    }

functionparseMarkdown(content) {
// 处理标题
        content = content.replace(/^#\s(.+)$/gm, '&lt;h1&gt;$1&lt;/h1&gt;');
        content = content.replace(/^##\s(.+)$/gm, '&lt;h2&gt;$1&lt;/h2&gt;');
        content = content.replace(/^###\s(.+)$/gm, '&lt;h3&gt;$1&lt;/h3&gt;');

// 处理图片 !(url)
        content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '&lt;img src=&quot;$2&quot; alt=&quot;$1&quot; style=&quot;max-width:100%;border-radius:6px;margin:10px 0;&quot;&gt;');

// 处理链接 (url)
        content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '&lt;a href=&quot;$2&quot; target=&quot;_blank&quot;&gt;$1&lt;/a&gt;');

// 处理粗体 **text**
        content = content.replace(/\*\*([^*]+)\*\*/g, '&lt;strong&gt;$1&lt;/strong&gt;');

// 处理斜体 *text*
        content = content.replace(/\*([^*]+)\*/g, '&lt;em&gt;$1&lt;/em&gt;');

// 处理代码块 `code`
        content = content.replace(/`([^`]+)`/g, '&lt;code&gt;$1&lt;/code&gt;');

return content;
    }

functionparseSpecialLinks(content) {
// 定义各种平台的正则表达式
constBILIBILI_REG = /https:\/\/www\.bilibili\.com\/video\/((av[\d]{1,10})|(BV[\w]{10}))\/?/g;
constBILIBILI_A_TAG_REG = /&lt;a\shref=&quot;https:\/\/www\.bilibili\.com\/video\/((av[\d]{1,10})|(BV[\w]{10}))\/?&quot;&gt;.*&lt;\/a&gt;/g;
constQQMUSIC_REG = /&lt;a\shref=&quot;https\:\/\/y\.qq\.com\/.*(\/+)(\.html)?&quot;.*?&gt;.*?&lt;\/a&gt;/g;
constQQVIDEO_REG = /&lt;a\shref=&quot;https:\/\/v\.qq\.com\/.*\/(+)\.html&quot;.*?&gt;.*?&lt;\/a&gt;/g;
constSPOTIFY_REG = /&lt;a\shref=&quot;https:\/\/open\.spotify\.com\/(track|album)\/([\s\S]+)&quot;.*?&gt;.*?&lt;\/a&gt;/g;
constYOUKU_REG = /&lt;a\shref=&quot;https:\/\/v\.youku\.com\/.*\/id_(+)\.html&quot;.*?&gt;.*&lt;\/a&gt;/g;
constYOUTUBE_REG = /&lt;a\shref=&quot;https:\/\/(www\.youtube\.com\/watch\?v=|youtu\.be\/)({11})&quot;.*?&gt;.*&lt;\/a&gt;/g;
constNETEASE_MUSIC_REG = /&lt;a\shref=&quot;https?:\/\/music\.163\.com\/.*?id=(\d+)&lt;\/a&gt;/g;

// 解析各种链接
return content
            .replace(BILIBILI_REG, &quot;&lt;div class='video-wrapper'&gt;&lt;iframe src='https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=$1&amp;as_wide=1&amp;high_quality=1&amp;danmaku=0' scrolling='no' border='0' frameborder='no' framespacing='0' allowfullscreen='true' style='position:absolute;height:100%;width:100%;'&gt;&lt;/iframe&gt;&lt;/div&gt;&quot;)
            .replace(YOUTUBE_REG, &quot;&lt;div class='video-wrapper'&gt;&lt;iframe src='https://www.youtube.com/embed/$2' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;&quot;)
            .replace(NETEASE_MUSIC_REG, &quot;&lt;div class='music-wrapper'&gt;&lt;meting-js auto='https://music.163.com/#/song?id=$1'&gt;&lt;/meting-js&gt;&lt;/div&gt;&quot;)
            .replace(QQMUSIC_REG, &quot;&lt;meting-js auto='https://y.qq.com/n/yqq/song$1.html'&gt;&lt;/meting-js&gt;&quot;)
            .replace(QQVIDEO_REG, &quot;&lt;div class='video-wrapper'&gt;&lt;iframe src='//v.qq.com/iframe/player.html?vid=$1' allowFullScreen='true' frameborder='no'&gt;&lt;/iframe&gt;&lt;/div&gt;&quot;)
            .replace(SPOTIFY_REG, &quot;&lt;div class='spotify-wrapper'&gt;&lt;iframe style='border-radius:12px' src='https://open.spotify.com/embed/$1/$2?utm_source=generator&amp;theme=0' width='100%' frameBorder='0' allowfullscreen='' allow='autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture' loading='lazy'&gt;&lt;/iframe&gt;&lt;/div&gt;&quot;)
            .replace(YOUKU_REG, &quot;&lt;div class='video-wrapper'&gt;&lt;iframe src='https://player.youku.com/embed/$1' frameborder=0 'allowfullscreen'&gt;&lt;/iframe&gt;&lt;/div&gt;&quot;);
    }

functionformatDate(dateString) {
if (!dateString) return'未知时间';
returnnewDate(dateString).toLocaleString();
    }
});
</code></pre>
<p>示例note.css</p>
<pre><code>/* 基础卡片样式 */
.rssmergecard {
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    margin-bottom: 20px;
    padding: 20px;
    transition: all 0.3s ease;
}

.rssmergecard:hover {
    box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}

/* 标题样式 */
.rssmergecard-title {
    color: #333;
    font-size: 18px;
    margin: 0 0 10px 0;
}

/* 内容样式 - 支持Markdown渲染 */
.rssmergecard-description {
    color: #555;
    line-height: 1.6;
    font-size: 15px;
}

.rssmergecard-description p {
    margin: 10px 0;
}

.rssmergecard-description a {
    color: #3498db;
    text-decoration: none;
}

.rssmergecard-description a:hover {
    text-decoration: underline;
}

.rssmergecard-description img {
    max-width: 100%;
    height: auto;
    border-radius: 4px;
}

/* 元信息样式 */
.rssmergecard-meta {
    margin-top: 15px;
    font-size: 13px;
    color: #999;
}

/* 加载更多按钮样式 */
.load-more {
    background: #3498db;
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    margin: 20px auto;
    display: block;
}

.load-more:hover {
    background: #2980b9;
}

.loaded-all {
    text-align: center;
    color: #999;
    font-size: 14px;
    margin: 20px 0;
}

/* 特殊链接卡片样式 */
.media-card {
    background: #f8f9fa;
    border-left: 4px solid #3498db;
    padding: 15px;
    margin: 15px 0;
    border-radius: 0 4px 4px 0;
}

.media-card-title {
    font-weight: bold;
    margin-bottom: 5px;
}

.video-wrapper {
    position: relative;
    padding-bottom: 56.25%; /* 16:9 */
    height: 0;
    margin: 15px 0;
}

.video-wrapper iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border-radius: 8px;
}

.music-wrapper, .spotify-wrapper {
    margin: 15px 0;
    border-radius: 8px;
    overflow: hidden;
    min-height: 86px; /* 确保有足够高度显示播放器 */
}

.music-wrapper meting-js {
    width: 100%;
    height: 86px;
}
</code></pre>
<p>ios快捷指令</p>
<p>使用快捷指令发布内容到站内,获取:https://www.icloud.com/shortcuts/8ba1240ab39d4bf2b4a02b69a5cc12bf</p>
<p><img src="https://www.3bbs.cn/index-diy/img.php?url=https://mmbiz.qpic.cn/sz_mmbiz_png/JVqvpxA6UlLiaWTORzgYkKpyYCvOrdJFD4vo9b3a0sr1nZ98RibDsCq4FxMgbjRyCypibric5arqz3UlGwpvzqvyEQ/640?wx_fmt=png&amp;from=appmsg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="idpz8Ea9DQMfyex" /></p>
<h2>问题🙋</h2>
<p>数据库可以直接迁移吗</p>
<p>1、直接上传至部署时挂载的路径中,重新启用,或者在容器文件夹/app/data/noise.db直接替换即可</p>
<p>2、使用后台数据库管理备份功能,支持一键下载、上传</p>
<p>数据库文件下载为zip格式,上传也必须为zip,本地数据库恢复包中必须有noise.db文件</p>
<h2>关于魔改指南🌈</h2>
<p>👉如何自定义化前端数据后添加到数据库?</p>
<p>需要在setting.go、migrate.go、models.go、controllers.go同时写入前端参数的后端定义,并修改前端参数信息为后端可读取的参数,其中controllers.go为控制器</p>
<ul>
<li>database.go 用于数据库连接管理</li>
<li>migrate.go 用于数据库迁移和数据初始化</li>
</ul>
<p>👉前端基本在web目录下,目前模版文件为components目录文件,pages下index.vue为父级模版</p>
<p>👉建议:不要和我一样在同一个文件里修改添加,造成一个文件上千行代码...请尽量使用父子层级来添加代码</p>
<p>最后,希望你能喜欢它!</p>

猫大仙 发表于 2025-4-17 20:32:50

第一感觉就是程序很不错,界面很好看,安装很复杂。

七夏 发表于 2025-4-17 21:32:41

猫大仙 发表于 2025-4-17 20:32
第一感觉就是程序很不错,界面很好看,安装很复杂。

哈哈,是有点复杂

梦淡如非 发表于 3 天前

好复杂。
页: [1]
查看完整版本: 完善的轻量说说笔记系统及memos的开源替代品-你的私人朋友圈