NewLife/YuQue

成功同步文章列表
大石头 authored at 2022-04-11 15:11:12
0823dd4
Tree
1 Parent(s) 7cf933f
Summary: 11 changed files with 336 additions and 85 deletions.
Modified +6 -4
Modified +10 -68
Modified +2 -2
Added +25 -0
Added +31 -0
Modified +7 -4
Modified +36 -3
Modified +51 -3
Modified +1 -1
Added +164 -0
Modified +3 -0
Modified +6 -4
diff --git a/NewLife.YuQue/YuqueClient.cs b/NewLife.YuQue/YuqueClient.cs
index 2463a66..5326b57 100644
--- a/NewLife.YuQue/YuqueClient.cs
+++ b/NewLife.YuQue/YuqueClient.cs
@@ -623,26 +623,28 @@ namespace NewLife.YuQue
         /// 获取一个仓库的文档列表
         /// </summary>
         /// <param name="bookName">仓库路径</param>
+        /// <param name="offset">用于分页,效果类似 MySQL 的 limit offset,一页 20 条</param>
         /// <returns></returns>
         /// <exception cref="ArgumentNullException"></exception>
-        public virtual async Task<Document[]> GetDocuments(String bookName)
+        public virtual async Task<Document[]> GetDocuments(String bookName, Int32 offset = 0)
         {
             if (bookName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(bookName));
 
-            return await GetAsync<Document[]>($"/repos/{bookName}/docs");
+            return await GetAsync<Document[]>($"/repos/{bookName}/docs", new { offset });
         }
 
         /// <summary>
         /// 获取一个仓库的文档列表
         /// </summary>
         /// <param name="bookId">仓库编号</param>
+        /// <param name="offset">用于分页,效果类似 MySQL 的 limit offset,一页 20 条</param>
         /// <returns></returns>
         /// <exception cref="ArgumentNullException"></exception>
-        public virtual async Task<Document[]> GetDocuments(Int32 bookId)
+        public virtual async Task<Document[]> GetDocuments(Int32 bookId, Int32 offset = 0)
         {
             if (bookId <= 0) throw new ArgumentNullException(nameof(bookId));
 
-            return await GetAsync<Document[]>($"/repos/{bookId}/docs");
+            return await GetAsync<Document[]>($"/repos/{bookId}/docs", new { offset });
         }
 
         /// <summary>
Modified +10 -68
diff --git a/NewLife.YuqueWeb/Areas/Yuque/Controllers/BookController.cs b/NewLife.YuqueWeb/Areas/Yuque/Controllers/BookController.cs
index 43b7ed1..15065f7 100644
--- a/NewLife.YuqueWeb/Areas/Yuque/Controllers/BookController.cs
+++ b/NewLife.YuqueWeb/Areas/Yuque/Controllers/BookController.cs
@@ -1,7 +1,7 @@
 using Microsoft.AspNetCore.Mvc;
 using NewLife.Cube;
 using NewLife.Web;
-using NewLife.YuQue;
+using NewLife.YuqueWeb.Services;
 using NewLife.YuQueWeb.Entity;
 using XCode.Membership;
 
@@ -11,6 +11,10 @@ namespace NewLife.YuqueWeb.Areas.Yuque.Controllers
     [Menu(90, true, Icon = "fa-tachometer")]
     public class BookController : EntityController<Book>
     {
+        private readonly BookService _bookService;
+
+        public BookController(BookService bookService) => _bookService = bookService;
+
         protected override IEnumerable<Book> Search(Pager p)
         {
             var enable = p["enable"]?.ToBoolean();
@@ -30,14 +34,10 @@ namespace NewLife.YuqueWeb.Areas.Yuque.Controllers
         {
             var count = 0;
             var ids = GetRequest("keys").SplitAsInt();
-            if (ids.Length > 0)
-                foreach (var id in ids)
-                {
-                    var team = Book.FindById(id);
-                    if (team != null)
-                    {
-                    }
-                }
+            foreach (var id in ids.OrderBy(e => e))
+            {
+                count += await _bookService.Sync(id);
+            }
 
             return JsonRefresh($"共刷新[{count}]个知识库");
         }
@@ -47,65 +47,7 @@ namespace NewLife.YuqueWeb.Areas.Yuque.Controllers
         [EntityAuthorize(PermissionFlags.Insert)]
         public async Task<ActionResult> ScanAll()
         {
-            var count = 0;
-
-            // 获取令牌
-            var p = Parameter.GetOrAdd(0, "Yuque", "Token");
-            if (p.Value.IsNullOrEmpty())
-            {
-                if (p.Remark.IsNullOrEmpty())
-                {
-                    p.Remark = "访问语雀的令牌,账户设置/Token,https://www.yuque.com/settings/tokens";
-                    p.Update();
-                }
-
-                throw new Exception("未设置令牌![系统管理/字典参数/Yuque/Token]");
-            }
-
-            var client = new YuqueClient { Token = p.Value };
-            var user = await client.GetUser();
-
-            var list = Book.FindAll();
-            var offset = 0;
-            while (true)
-            {
-                var repos = await client.GetRepos(user.Id, "all", offset);
-                if (repos.Length == 0) break;
-
-                foreach (var repo in repos)
-                {
-                    var book = list.FirstOrDefault(e => e.Id == repo.Id || e.Slug == repo.Slug);
-                    if (book == null)
-                    {
-                        book = new Book
-                        {
-                            Id = repo.Id,
-                            Name = repo.Name,
-                            Slug = repo.Slug,
-                            Code = repo.Slug,
-                            Enable = true
-                        };
-                        book.Insert();
-
-                        list.Add(book);
-                    }
-
-                    book.Name = repo.Name;
-                    book.Type = repo.Type;
-                    book.UserName = repo.User?.Name;
-                    book.Docs = repo.Items;
-                    book.Likes = repo.Likes;
-                    book.Watches = repo.Watches;
-                    book.Namespace = repo.Namespace;
-                    book.Remark = repo.Description;
-                    book.SyncTime = DateTime.Now;
-
-                    book.Update();
-                }
-
-                count += repos.Length;
-                offset += repos.Length;
-            }
+            var count = await _bookService.ScanAll();
 
             return JsonRefresh($"共扫描[{count}]个知识库");
         }
Modified +2 -2
diff --git a/NewLife.YuqueWeb/Areas/Yuque/Views/Book/_List_Toolbar_Batch.cshtml b/NewLife.YuqueWeb/Areas/Yuque/Views/Book/_List_Toolbar_Batch.cshtml
index d0ea3c4..a8ff85c 100644
--- a/NewLife.YuqueWeb/Areas/Yuque/Views/Book/_List_Toolbar_Batch.cshtml
+++ b/NewLife.YuqueWeb/Areas/Yuque/Views/Book/_List_Toolbar_Batch.cshtml
@@ -11,9 +11,9 @@
 @if (set.EnableSelect)
 {
     <button type="button" class="btn btn-purple btn-sm" data-action="action" data-url="@Url.Action("SyncRepo")" data-fields="keys" disabled>
-        同步知识库
+        同步文章列表
     </button>
     <button type="button" class="btn btn-purple btn-sm" data-action="action" data-url="@Url.Action("ScanAll")" >
-        扫描全部知识库
+        扫描知识库
     </button>
 }
\ No newline at end of file
Added +25 -0
diff --git a/NewLife.YuqueWeb/Entity/Config/Core.config b/NewLife.YuqueWeb/Entity/Config/Core.config
new file mode 100644
index 0000000..1a42483
--- /dev/null
+++ b/NewLife.YuqueWeb/Entity/Config/Core.config
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Core>
+  <!--全局调试。XTrace.Debug-->
+  <Debug>true</Debug>
+  <!--日志等级。只输出大于等于该级别的日志,All/Debug/Info/Warn/Error/Fatal,默认Info-->
+  <LogLevel>Info</LogLevel>
+  <!--文件日志目录。默认Log子目录,web上一级Log-->
+  <LogPath>Log</LogPath>
+  <!--日志文件上限。超过上限后拆分新日志文件,默认10MB,0表示不限制大小-->
+  <LogFileMaxBytes>10</LogFileMaxBytes>
+  <!--日志文件备份。超过备份数后,最旧的文件将被删除,默认100,0表示不限制个数-->
+  <LogFileBackups>100</LogFileBackups>
+  <!--日志文件格式。默认{0:yyyy_MM_dd}.log,支持日志等级如 {1}_{0:yyyy_MM_dd}.log-->
+  <LogFileFormat>{0:yyyy_MM_dd}.log</LogFileFormat>
+  <!--网络日志。本地子网日志广播udp://255.255.255.255:514,或者http://xxx:80/log-->
+  <NetworkLog></NetworkLog>
+  <!--数据目录。本地数据库目录,默认Data子目录,web上一级Data-->
+  <DataPath>Data</DataPath>
+  <!--备份目录。备份数据库时存放的目录,默认Backup子目录,web上一级Backup-->
+  <BackupPath>Backup</BackupPath>
+  <!--插件目录-->
+  <PluginPath>Plugins</PluginPath>
+  <!--插件服务器。将从该网页上根据关键字分析链接并下载插件-->
+  <PluginServer>http://x.newlifex.com/</PluginServer>
+</Core>
\ No newline at end of file
Added +31 -0
diff --git a/NewLife.YuqueWeb/Entity/Config/XCode.config b/NewLife.YuqueWeb/Entity/Config/XCode.config
new file mode 100644
index 0000000..149162e
--- /dev/null
+++ b/NewLife.YuqueWeb/Entity/Config/XCode.config
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<XCode>
+  <!--调试-->
+  <Debug>true</Debug>
+  <!--输出SQL。是否输出SQL语句,默认启用-->
+  <ShowSQL>true</ShowSQL>
+  <!--SQL目录。设置SQL输出的单独目录,默认为空,SQL输出到当前日志中。生产环境建议输出到站点外单独的SqlLog目录-->
+  <SQLPath></SQLPath>
+  <!--SQL执行时间。跟踪SQL执行时间,大于该阀值将输出日志,默认1000毫秒-->
+  <TraceSQLTime>1000</TraceSQLTime>
+  <!--SQL最大长度。输出日志时的SQL最大长度,超长截断,默认4096,不截断用0-->
+  <SQLMaxLength>4096</SQLMaxLength>
+  <!--参数化添删改查。默认关闭-->
+  <UseParameter>false</UseParameter>
+  <!--命令超时。查询执行超时时间,默认0秒不限制-->
+  <CommandTimeout>0</CommandTimeout>
+  <!--失败重试。执行命令超时后的重试次数,默认0不重试-->
+  <RetryOnFailure>0</RetryOnFailure>
+  <!--数据层缓存。根据sql做缓存,默认0秒-->
+  <DataCacheExpire>0</DataCacheExpire>
+  <!--实体缓存过期。整表缓存实体列表,默认10秒-->
+  <EntityCacheExpire>10</EntityCacheExpire>
+  <!--单对象缓存过期。按主键缓存实体,默认10秒-->
+  <SingleCacheExpire>10</SingleCacheExpire>
+  <!--扩展属性过期。扩展属性Extends缓存,默认10秒-->
+  <ExtendExpire>10</ExtendExpire>
+  <!--反向工程。Off 关闭;ReadOnly 只读不执行;On 打开,仅新建;Full 完全,修改删除-->
+  <Migration>On</Migration>
+  <!--表名称、字段名大小写格式。Default 根据模型生成;Upper 全大写;Lower 全小写;Underline下划线-->
+  <NameFormat>Default</NameFormat>
+</XCode>
\ No newline at end of file
Modified +7 -4
diff --git a/NewLife.YuqueWeb/Entity/Model.xml b/NewLife.YuqueWeb/Entity/Model.xml
index 49e087d..bfd0d80 100644
--- a/NewLife.YuqueWeb/Entity/Model.xml
+++ b/NewLife.YuqueWeb/Entity/Model.xml
@@ -2,7 +2,7 @@
 <Tables xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xs:schemaLocation="http://www.newlifex.com http://www.newlifex.com/Model2022.xsd" NameSpace="NewLife.YuQueWeb.Entity" ConnName="YuQue" Output="" BaseClass="Entity" Version="11.0.2022.0405" Document="https://www.yuque.com/smartstone/xcode/model" xmlns="http://www.newlifex.com/Model2022.xsd">
   <Table Name="Book" Description="知识库。管理知识库">
     <Columns>
-      <Column Name="Id" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
+      <Column Name="Id" DataType="Int32" PrimaryKey="True" Description="编号" />
       <Column Name="Code" DataType="String" Description="编码。路径唯一标识,默认取Slug" />
       <Column Name="Name" DataType="String" Master="True" Nullable="False" Description="名称" />
       <Column Name="Type" DataType="String" Description="类型" />
@@ -32,9 +32,9 @@
   </Table>
   <Table Name="Document" Description="文档。文档内容">
     <Columns>
-      <Column Name="Id" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
+      <Column Name="Id" DataType="Int32" PrimaryKey="True" Description="编号" />
       <Column Name="Code" DataType="String" Description="编码。路径唯一标识,默认取Slug" />
-      <Column Name="Title" DataType="String" Master="True" Nullable="False" Description="标题" />
+      <Column Name="Title" DataType="String" Master="True" Length="200" Nullable="False" Description="标题" />
       <Column Name="BookId" DataType="Int32" Description="知识库" />
       <Column Name="Enable" DataType="Boolean" Description="启用" />
       <Column Name="UserName" DataType="String" Description="用户" />
@@ -48,7 +48,10 @@
       <Column Name="PublishTime" DataType="DateTime" Description="发布时间" />
       <Column Name="FirstPublishTime" DataType="DateTime" Description="首次发布" />
       <Column Name="WordCount" DataType="Int32" Description="单词数" />
-      <Column Name="Cover" DataType="String" Description="封面" />
+      <Column Name="Cover" DataType="String" Length="200" Description="封面" />
+      <Column Name="Slug" DataType="String" Description="路径" />
+      <Column Name="Sync" DataType="Boolean" Description="同步。是否自动同步远程内容" />
+      <Column Name="SyncTime" DataType="DateTime" Description="同步时间。最后一次同步数据的时间" />
       <Column Name="CreateUser" DataType="String" Description="创建者" Model="False" />
       <Column Name="CreateUserID" DataType="Int32" Description="创建人" Model="False" />
       <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" />
Modified +36 -3
diff --git a/NewLife.YuqueWeb/Entity/YuQue.htm b/NewLife.YuqueWeb/Entity/YuQue.htm
index 20d9e6a..4d751b5 100644
--- a/NewLife.YuqueWeb/Entity/YuQue.htm
+++ b/NewLife.YuqueWeb/Entity/YuQue.htm
@@ -55,7 +55,7 @@
             <td>Int32</td>
             <td></td>
             <td></td>
-            <td title="自增">AI</td>
+            <td title="主键">PK</td>
             <td>N</td>
             <td></td>
         </tr>
@@ -314,7 +314,7 @@
             <td>Int32</td>
             <td></td>
             <td></td>
-            <td title="自增">AI</td>
+            <td title="主键">PK</td>
             <td>N</td>
             <td></td>
         </tr>
@@ -334,7 +334,7 @@
             <td>Title</td>
             <td>标题</td>
             <td>String</td>
-            <td>50</td>
+            <td>200</td>
             <td></td>
             <td></td>
             <td>N</td>
@@ -488,6 +488,17 @@
             <td>Cover</td>
             <td>封面</td>
             <td>String</td>
+            <td>200</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Slug</td>
+            <td>路径</td>
+            <td>String</td>
             <td>50</td>
             <td></td>
             <td></td>
@@ -496,6 +507,28 @@
         </tr>
 
         <tr>
+            <td>Sync</td>
+            <td>同步</td>
+            <td>Boolean</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td>是否自动同步远程内容</td>
+        </tr>
+
+        <tr>
+            <td>SyncTime</td>
+            <td>同步时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>最后一次同步数据的时间</td>
+        </tr>
+
+        <tr>
             <td>CreateUser</td>
             <td>创建者</td>
             <td>String</td>
Modified +51 -3
diff --git "a/NewLife.YuqueWeb/Entity/\346\226\207\346\241\243.cs" "b/NewLife.YuqueWeb/Entity/\346\226\207\346\241\243.cs"
index b68f759..442fc6a 100644
--- "a/NewLife.YuqueWeb/Entity/\346\226\207\346\241\243.cs"
+++ "b/NewLife.YuqueWeb/Entity/\346\226\207\346\241\243.cs"
@@ -25,7 +25,7 @@ namespace NewLife.YuQueWeb.Entity
         /// <summary>编号</summary>
         [DisplayName("编号")]
         [Description("编号")]
-        [DataObjectField(true, true, false, 0)]
+        [DataObjectField(true, false, false, 0)]
         [BindColumn("Id", "编号", "")]
         public Int32 Id { get => _Id; set { if (OnPropertyChanging("Id", value)) { _Id = value; OnPropertyChanged("Id"); } } }
 
@@ -41,7 +41,7 @@ namespace NewLife.YuQueWeb.Entity
         /// <summary>标题</summary>
         [DisplayName("标题")]
         [Description("标题")]
-        [DataObjectField(false, false, false, 50)]
+        [DataObjectField(false, false, false, 200)]
         [BindColumn("Title", "标题", "", Master = true)]
         public String Title { get => _Title; set { if (OnPropertyChanging("Title", value)) { _Title = value; OnPropertyChanged("Title"); } } }
 
@@ -153,10 +153,34 @@ namespace NewLife.YuQueWeb.Entity
         /// <summary>封面</summary>
         [DisplayName("封面")]
         [Description("封面")]
-        [DataObjectField(false, false, true, 50)]
+        [DataObjectField(false, false, true, 200)]
         [BindColumn("Cover", "封面", "")]
         public String Cover { get => _Cover; set { if (OnPropertyChanging("Cover", value)) { _Cover = value; OnPropertyChanged("Cover"); } } }
 
+        private String _Slug;
+        /// <summary>路径</summary>
+        [DisplayName("路径")]
+        [Description("路径")]
+        [DataObjectField(false, false, true, 50)]
+        [BindColumn("Slug", "路径", "")]
+        public String Slug { get => _Slug; set { if (OnPropertyChanging("Slug", value)) { _Slug = value; OnPropertyChanged("Slug"); } } }
+
+        private Boolean _Sync;
+        /// <summary>同步。是否自动同步远程内容</summary>
+        [DisplayName("同步")]
+        [Description("同步。是否自动同步远程内容")]
+        [DataObjectField(false, false, false, 0)]
+        [BindColumn("Sync", "同步。是否自动同步远程内容", "")]
+        public Boolean Sync { get => _Sync; set { if (OnPropertyChanging("Sync", value)) { _Sync = value; OnPropertyChanged("Sync"); } } }
+
+        private DateTime _SyncTime;
+        /// <summary>同步时间。最后一次同步数据的时间</summary>
+        [DisplayName("同步时间")]
+        [Description("同步时间。最后一次同步数据的时间")]
+        [DataObjectField(false, false, true, 0)]
+        [BindColumn("SyncTime", "同步时间。最后一次同步数据的时间", "")]
+        public DateTime SyncTime { get => _SyncTime; set { if (OnPropertyChanging("SyncTime", value)) { _SyncTime = value; OnPropertyChanged("SyncTime"); } } }
+
         private String _CreateUser;
         /// <summary>创建者</summary>
         [DisplayName("创建者")]
@@ -257,6 +281,9 @@ namespace NewLife.YuQueWeb.Entity
                     case "FirstPublishTime": return _FirstPublishTime;
                     case "WordCount": return _WordCount;
                     case "Cover": return _Cover;
+                    case "Slug": return _Slug;
+                    case "Sync": return _Sync;
+                    case "SyncTime": return _SyncTime;
                     case "CreateUser": return _CreateUser;
                     case "CreateUserID": return _CreateUserID;
                     case "CreateIP": return _CreateIP;
@@ -290,6 +317,9 @@ namespace NewLife.YuQueWeb.Entity
                     case "FirstPublishTime": _FirstPublishTime = value.ToDateTime(); break;
                     case "WordCount": _WordCount = value.ToInt(); break;
                     case "Cover": _Cover = Convert.ToString(value); break;
+                    case "Slug": _Slug = Convert.ToString(value); break;
+                    case "Sync": _Sync = value.ToBoolean(); break;
+                    case "SyncTime": _SyncTime = value.ToDateTime(); break;
                     case "CreateUser": _CreateUser = Convert.ToString(value); break;
                     case "CreateUserID": _CreateUserID = value.ToInt(); break;
                     case "CreateIP": _CreateIP = Convert.ToString(value); break;
@@ -360,6 +390,15 @@ namespace NewLife.YuQueWeb.Entity
             /// <summary>封面</summary>
             public static readonly Field Cover = FindByName("Cover");
 
+            /// <summary>路径</summary>
+            public static readonly Field Slug = FindByName("Slug");
+
+            /// <summary>同步。是否自动同步远程内容</summary>
+            public static readonly Field Sync = FindByName("Sync");
+
+            /// <summary>同步时间。最后一次同步数据的时间</summary>
+            public static readonly Field SyncTime = FindByName("SyncTime");
+
             /// <summary>创建者</summary>
             public static readonly Field CreateUser = FindByName("CreateUser");
 
@@ -444,6 +483,15 @@ namespace NewLife.YuQueWeb.Entity
             /// <summary>封面</summary>
             public const String Cover = "Cover";
 
+            /// <summary>路径</summary>
+            public const String Slug = "Slug";
+
+            /// <summary>同步。是否自动同步远程内容</summary>
+            public const String Sync = "Sync";
+
+            /// <summary>同步时间。最后一次同步数据的时间</summary>
+            public const String SyncTime = "SyncTime";
+
             /// <summary>创建者</summary>
             public const String CreateUser = "CreateUser";
 
Modified +1 -1
diff --git "a/NewLife.YuqueWeb/Entity/\347\237\245\350\257\206\345\272\223.cs" "b/NewLife.YuqueWeb/Entity/\347\237\245\350\257\206\345\272\223.cs"
index e0c5f95..0d2fa78 100644
--- "a/NewLife.YuqueWeb/Entity/\347\237\245\350\257\206\345\272\223.cs"
+++ "b/NewLife.YuqueWeb/Entity/\347\237\245\350\257\206\345\272\223.cs"
@@ -24,7 +24,7 @@ namespace NewLife.YuQueWeb.Entity
         /// <summary>编号</summary>
         [DisplayName("编号")]
         [Description("编号")]
-        [DataObjectField(true, true, false, 0)]
+        [DataObjectField(true, false, false, 0)]
         [BindColumn("Id", "编号", "")]
         public Int32 Id { get => _Id; set { if (OnPropertyChanging("Id", value)) { _Id = value; OnPropertyChanged("Id"); } } }
 
Added +164 -0
diff --git a/NewLife.YuqueWeb/Services/BookService.cs b/NewLife.YuqueWeb/Services/BookService.cs
new file mode 100644
index 0000000..4f67e37
--- /dev/null
+++ b/NewLife.YuqueWeb/Services/BookService.cs
@@ -0,0 +1,164 @@
+using NewLife.Log;
+using NewLife.YuQue;
+using NewLife.YuQueWeb.Entity;
+using XCode.Membership;
+
+namespace NewLife.YuqueWeb.Services;
+
+/// <summary>
+/// 知识库服务
+/// </summary>
+public class BookService
+{
+    private readonly ITracer _tracer;
+
+    /// <summary>
+    /// 实例化知识库服务
+    /// </summary>
+    /// <param name="tracer"></param>
+    public BookService(ITracer tracer)
+    {
+        _tracer = tracer;
+    }
+
+    /// <summary>
+    /// 扫描发现所有知识库
+    /// </summary>
+    /// <returns></returns>
+    public async Task<Int32> ScanAll()
+    {
+        var count = 0;
+        var token = GetToken();
+
+        var client = new YuqueClient { Token = token, Log = XTrace.Log, Tracer = _tracer };
+        var user = await client.GetUser();
+
+        var list = Book.FindAll();
+        var offset = 0;
+        while (true)
+        {
+            var repos = await client.GetRepos(user.Id, "all", offset);
+            if (repos.Length == 0) break;
+
+            foreach (var item in repos)
+            {
+                var book = list.FirstOrDefault(e => e.Id == item.Id || e.Slug == item.Slug);
+                if (book == null)
+                {
+                    book = new Book
+                    {
+                        Id = item.Id,
+                        Name = item.Name,
+                        Slug = item.Slug,
+                        Code = item.Slug,
+                        Enable = true,
+                        Sync = true,
+                    };
+                    book.Insert();
+
+                    list.Add(book);
+                }
+
+                book.Name = item.Name;
+                book.Type = item.Type;
+                book.UserName = item.User?.Name;
+                book.Docs = item.Items;
+                book.Likes = item.Likes;
+                book.Watches = item.Watches;
+                book.Namespace = item.Namespace;
+                book.Remark = item.Description;
+                book.SyncTime = DateTime.Now;
+                book.CreateTime = item.CreateTime;
+                book.UpdateTime = item.UpdateTime;
+
+                book.Update();
+            }
+
+            count += repos.Length;
+            offset += repos.Length;
+        }
+
+        return count;
+    }
+
+    String GetToken()
+    {
+        // 获取令牌
+        var p = Parameter.GetOrAdd(0, "Yuque", "Token");
+        if (p.Value.IsNullOrEmpty())
+        {
+            if (p.Remark.IsNullOrEmpty())
+            {
+                p.Remark = "访问语雀的令牌,账户设置/Token,https://www.yuque.com/settings/tokens";
+                p.Update();
+            }
+
+            throw new Exception("未设置令牌![系统管理/字典参数/Yuque/Token]");
+        }
+
+        return p.Value;
+    }
+
+    /// <summary>
+    /// 同步指定知识库之下的文章列表
+    /// </summary>
+    /// <param name="bookId"></param>
+    /// <returns></returns>
+    public async Task<Int32> Sync(Int32 bookId)
+    {
+        var book = Book.FindById(bookId);
+        if (book == null || !book.Sync) return 0;
+
+        var token = GetToken();
+        var client = new YuqueClient { Token = token, Log = XTrace.Log, Tracer = _tracer };
+
+        var count = 0;
+        var offset = 0;
+        while (true)
+        {
+            var list = await client.GetDocuments(book.Id, offset);
+            if (list.Length == 0) break;
+
+            foreach (var item in list)
+            {
+                var doc = Document.FindByCode(item.Slug);
+                if (doc == null)
+                {
+                    doc = new Document
+                    {
+                        Id = item.Id,
+                        Code = item.Slug,
+                        Title = item.Title,
+                        BookId = bookId,
+                        Slug = item.Slug,
+                        Enable = true,
+                        Sync = true
+                    };
+                    doc.Insert();
+                }
+
+                doc.UserName = item.LastEditor?.Name;
+                doc.Format = item.Format;
+                //doc.Hits = item.Hits;
+                doc.Likes = item.Likes;
+                doc.Comments = item.Comments;
+                doc.WordCount = item.WordCount;
+                doc.Cover = item.Cover;
+                doc.Remark = item.Description;
+
+                doc.SyncTime = DateTime.Now;
+                doc.PublishTime = item.PublishTime;
+                doc.PublishTime = item.FirstPublishTime;
+                doc.CreateTime = item.CreateTime;
+                doc.UpdateTime = item.UpdateTime;
+
+                doc.Update();
+            }
+
+            count += list.Length;
+            offset += list.Length;
+        }
+
+        return count;
+    }
+}
\ No newline at end of file
Modified +3 -0
diff --git a/NewLife.YuqueWeb/YuqueService.cs b/NewLife.YuqueWeb/YuqueService.cs
index 2f2759e..60600de 100644
--- a/NewLife.YuqueWeb/YuqueService.cs
+++ b/NewLife.YuqueWeb/YuqueService.cs
@@ -2,6 +2,7 @@
 using NewLife.Cube;
 using NewLife.Log;
 using NewLife.YuqueWeb.Areas.Yuque;
+using NewLife.YuqueWeb.Services;
 
 namespace NewLife.YuQueWeb;
 
@@ -18,6 +19,8 @@ public static class YuqueService
         XTrace.WriteLine("{0} Start 配置语雀 {0}", new String('=', 32));
         Assembly.GetExecutingAssembly().WriteVersion();
 
+        services.AddSingleton<BookService>();
+
         XTrace.WriteLine("{0} End   配置语雀 {0}", new String('=', 32));
 
         return services;